Merge pull request #2199 from dpalou/MOBILE-2235

Mobile 2235
main
Juan Leyva 2019-12-09 16:14:01 +01:00 committed by GitHub
commit a8ee898a91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
103 changed files with 15046 additions and 311 deletions

View File

@ -41,4 +41,8 @@ module.exports = {
src: ['{{ROOT}}/node_modules/mathjax/localization/**/*'],
dest: '{{WWW}}/lib/mathjax/localization'
},
copyH5P: {
src: ['{{ROOT}}/src/core/h5p/assets/**/*'],
dest: '{{WWW}}/h5p/'
},
};

View File

@ -1552,6 +1552,95 @@
"core.group": "moodle",
"core.groupsseparate": "moodle",
"core.groupsvisible": "moodle",
"core.h5p.additionallicenseinfo": "h5p",
"core.h5p.author": "h5p",
"core.h5p.authorcomments": "h5p",
"core.h5p.authorcommentsdescription": "h5p",
"core.h5p.authorname": "h5p",
"core.h5p.authorrole": "h5p",
"core.h5p.by": "h5p",
"core.h5p.cancellabel": "h5p",
"core.h5p.ccattribution": "h5p",
"core.h5p.ccattributionnc": "h5p",
"core.h5p.ccattributionncnd": "h5p",
"core.h5p.ccattributionncsa": "h5p",
"core.h5p.ccattributionnd": "h5p",
"core.h5p.ccattributionsa": "h5p",
"core.h5p.ccpdd": "h5p",
"core.h5p.changedby": "h5p",
"core.h5p.changedescription": "h5p",
"core.h5p.changelog": "h5p",
"core.h5p.changeplaceholder": "h5p",
"core.h5p.close": "h5p",
"core.h5p.confirmdialogbody": "h5p",
"core.h5p.confirmdialogheader": "h5p",
"core.h5p.confirmlabel": "h5p",
"core.h5p.connectionLost": "h5p",
"core.h5p.connectionReestablished": "h5p",
"core.h5p.contentCopied": "h5p",
"core.h5p.contentchanged": "h5p",
"core.h5p.contenttype": "h5p",
"core.h5p.copyright": "h5p",
"core.h5p.copyrightinfo": "h5p",
"core.h5p.copyrightstring": "h5p",
"core.h5p.copyrighttitle": "h5p",
"core.h5p.creativecommons": "h5p",
"core.h5p.date": "h5p",
"core.h5p.disablefullscreen": "h5p",
"core.h5p.download": "h5p",
"core.h5p.downloadtitle": "h5p",
"core.h5p.editor": "h5p",
"core.h5p.embed": "h5p",
"core.h5p.embedtitle": "h5p",
"core.h5p.fullscreen": "h5p",
"core.h5p.gpl": "h5p",
"core.h5p.h5ptitle": "h5p",
"core.h5p.hideadvanced": "h5p",
"core.h5p.license": "h5p",
"core.h5p.licenseCC010": "h5p",
"core.h5p.licenseCC010U": "h5p",
"core.h5p.licenseCC10": "h5p",
"core.h5p.licenseCC20": "h5p",
"core.h5p.licenseCC25": "h5p",
"core.h5p.licenseCC30": "h5p",
"core.h5p.licenseCC40": "h5p",
"core.h5p.licenseGPL": "h5p",
"core.h5p.licenseV1": "h5p",
"core.h5p.licenseV2": "h5p",
"core.h5p.licenseV3": "h5p",
"core.h5p.licensee": "h5p",
"core.h5p.licenseextras": "h5p",
"core.h5p.licenseversion": "h5p",
"core.h5p.nocopyright": "h5p",
"core.h5p.offlineDialogBody": "h5p",
"core.h5p.offlineDialogHeader": "h5p",
"core.h5p.offlineDialogRetryButtonLabel": "h5p",
"core.h5p.offlineDialogRetryMessage": "h5p",
"core.h5p.offlineSuccessfulSubmit": "h5p",
"core.h5p.originator": "h5p",
"core.h5p.pd": "h5p",
"core.h5p.pddl": "h5p",
"core.h5p.play": "local_moodlemobileapp",
"core.h5p.pdm": "h5p",
"core.h5p.resizescript": "h5p",
"core.h5p.resubmitScores": "h5p",
"core.h5p.reuse": "h5p",
"core.h5p.reuseContent": "h5p",
"core.h5p.reuseDescription": "h5p",
"core.h5p.showadvanced": "h5p",
"core.h5p.showless": "h5p",
"core.h5p.showmore": "h5p",
"core.h5p.size": "h5p",
"core.h5p.source": "h5p",
"core.h5p.startingover": "h5p",
"core.h5p.sublevel": "h5p",
"core.h5p.thumbnail": "h5p",
"core.h5p.title": "h5p",
"core.h5p.undisclosed": "h5p",
"core.h5p.year": "h5p",
"core.h5p.years": "h5p",
"core.h5p.yearsfrom": "h5p",
"core.h5p.yearsto": "h5p",
"core.hasdatatosync": "local_moodlemobileapp",
"core.help": "moodle",
"core.hide": "moodle",

View File

@ -0,0 +1,34 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { CoreFilterDelegate } from '@core/filter/providers/delegate';
import { AddonFilterDisplayH5PHandler } from './providers/handler';
@NgModule({
declarations: [
],
imports: [
IonicModule
],
providers: [
AddonFilterDisplayH5PHandler
]
})
export class AddonFilterDisplayH5PModule {
constructor(filterDelegate: CoreFilterDelegate, handler: AddonFilterDisplayH5PHandler) {
filterDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,98 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
import { CoreFilterDefaultHandler } from '@core/filter/providers/default-filter';
import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@core/filter/providers/filter';
import { CoreH5PPlayerComponent } from '@core/h5p/components/h5p-player/h5p-player';
/**
* Handler to support the Display H5P filter.
*/
@Injectable()
export class AddonFilterDisplayH5PHandler extends CoreFilterDefaultHandler {
name = 'AddonFilterDisplayH5PHandler';
filterName = 'displayh5p';
protected template = document.createElement('template'); // A template element to convert HTML to element.
constructor(protected factoryResolver: ComponentFactoryResolver) {
super();
}
/**
* Filter some text.
*
* @param text The text to filter.
* @param filter The filter.
* @param options Options passed to the filters.
* @param siteId Site ID. If not defined, current site.
* @return Filtered text (or promise resolved with the filtered text).
*/
filter(text: string, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string)
: string | Promise<string> {
this.template.innerHTML = text;
const h5pIframes = <HTMLIFrameElement[]> Array.from(this.template.content.querySelectorAll('iframe.h5p-iframe'));
// Replace all iframes with an empty div that will be treated in handleHtml.
h5pIframes.forEach((iframe) => {
const placeholder = document.createElement('div');
placeholder.classList.add('core-h5p-tmp-placeholder');
placeholder.setAttribute('data-player-src', iframe.src);
iframe.parentElement.replaceChild(placeholder, iframe);
});
return this.template.innerHTML;
}
/**
* Handle HTML. This function is called after "filter", and it will receive an HTMLElement containing the text that was
* filtered.
*
* @param container The HTML container to handle.
* @param filter The filter.
* @param options Options passed to the filters.
* @param viewContainerRef The ViewContainerRef where the container is.
* @param component Component.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return If async, promise resolved when done.
*/
handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions,
viewContainerRef: ViewContainerRef, component?: string, componentId?: string | number, siteId?: string)
: void | Promise<void> {
const placeholders = <HTMLElement[]> Array.from(container.querySelectorAll('div.core-h5p-tmp-placeholder'));
placeholders.forEach((placeholder) => {
const url = placeholder.getAttribute('data-player-src');
// Create the component to display the player.
const factory = this.factoryResolver.resolveComponentFactory(CoreH5PPlayerComponent),
componentRef = viewContainerRef.createComponent(factory);
componentRef.instance.src = url;
componentRef.instance.component = component;
componentRef.instance.componentId = componentId;
// Move the component to its right position.
placeholder.parentElement.replaceChild(componentRef.instance.elementRef.nativeElement, placeholder);
});
}
}

View File

@ -17,6 +17,7 @@ import { AddonFilterActivityNamesModule } from './activitynames/activitynames.mo
import { AddonFilterAlgebraModule } from './algebra/algebra.module';
import { AddonFilterCensorModule } from './censor/censor.module';
import { AddonFilterDataModule } from './data/data.module';
import { AddonFilterDisplayH5PModule } from './displayh5p/displayh5p.module';
import { AddonFilterEmailProtectModule } from './emailprotect/emailprotect.module';
import { AddonFilterEmoticonModule } from './emoticon/emoticon.module';
import { AddonFilterGlossaryModule } from './glossary/glossary.module';
@ -34,6 +35,7 @@ import { AddonFilterUrlToLinkModule } from './urltolink/urltolink.module';
AddonFilterAlgebraModule,
AddonFilterCensorModule,
AddonFilterDataModule,
AddonFilterDisplayH5PModule,
AddonFilterEmailProtectModule,
AddonFilterEmoticonModule,
AddonFilterGlossaryModule,

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Injectable, ViewContainerRef } from '@angular/core';
import { CoreFilterDefaultHandler } from '@core/filter/providers/default-filter';
import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@core/filter/providers/filter';
import { CoreEventsProvider } from '@providers/events';
@ -161,10 +161,14 @@ export class AddonFilterMathJaxLoaderHandler extends CoreFilterDefaultHandler {
* @param container The HTML container to handle.
* @param filter The filter.
* @param options Options passed to the filters.
* @param viewContainerRef The ViewContainerRef where the container is.
* @param component Component.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return If async, promise resolved when done.
*/
handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string)
handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions,
viewContainerRef: ViewContainerRef, component?: string, componentId?: string | number, siteId?: string)
: void | Promise<void> {
return this.waitForReady().then(() => {

View File

@ -27,6 +27,8 @@ export class AddonFilterMediaPluginHandler extends CoreFilterDefaultHandler {
name = 'AddonFilterMediaPluginHandler';
filterName = 'mediaplugin';
protected template = document.createElement('template'); // A template element to convert HTML to element.
constructor(private textUtils: CoreTextUtilsProvider,
private urlUtils: CoreUrlUtilsProvider) {
super();
@ -44,16 +46,15 @@ export class AddonFilterMediaPluginHandler extends CoreFilterDefaultHandler {
filter(text: string, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string)
: string | Promise<string> {
const div = document.createElement('div');
div.innerHTML = text;
this.template.innerHTML = text;
const videos = Array.from(div.querySelectorAll('video'));
const videos = Array.from(this.template.content.querySelectorAll('video'));
videos.forEach((video) => {
this.treatVideoFilters(video);
});
return div.innerHTML;
return this.template.innerHTML;
}
/**

View File

@ -32,6 +32,7 @@ import { AddonModAssignSyncProvider } from './assign-sync';
import { AddonModAssignFeedbackDelegate } from './feedback-delegate';
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch assigns.
@ -51,6 +52,7 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected assignProvider: AddonModAssignProvider,
protected textUtils: CoreTextUtilsProvider,
protected feedbackDelegate: AddonModAssignFeedbackDelegate,
@ -62,7 +64,8 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
protected assignHelper: AddonModAssignHelperProvider,
protected syncProvider: AddonModAssignSyncProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -23,6 +23,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler';
import { AddonModBookProvider } from './book';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch books.
@ -42,9 +43,11 @@ export class AddonModBookPrefetchHandler extends CoreCourseResourcePrefetchHandl
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected bookProvider: AddonModBookProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -25,6 +25,7 @@ import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/acti
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonModChatProvider, AddonModChatChat } from './chat';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch chats.
@ -43,11 +44,13 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
private groupsProvider: CoreGroupsProvider,
private userProvider: CoreUserProvider,
private chatProvider: AddonModChatProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -25,6 +25,7 @@ import { CoreUserProvider } from '@core/user/providers/user';
import { AddonModChoiceSyncProvider } from './sync';
import { AddonModChoiceProvider } from './choice';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch choices.
@ -46,11 +47,13 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected choiceProvider: AddonModChoiceProvider,
protected userProvider: CoreUserProvider,
protected injector: Injector) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -28,6 +28,7 @@ import { AddonModDataProvider, AddonModDataEntry } from './data';
import { AddonModDataSyncProvider } from './sync';
import { AddonModDataHelperProvider } from './helper';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch databases.
@ -47,6 +48,7 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected dataProvider: AddonModDataProvider,
protected timeUtils: CoreTimeUtilsProvider,
protected dataHelper: AddonModDataHelperProvider,
@ -54,7 +56,8 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
protected commentsProvider: CoreCommentsProvider,
protected syncProvider: AddonModDataSyncProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -27,6 +27,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreGroupsProvider } from '@providers/groups';
import { AddonModFeedbackSyncProvider } from './sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch feedbacks.
@ -48,13 +49,15 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected feedbackProvider: AddonModFeedbackProvider,
protected feedbackHelper: AddonModFeedbackHelperProvider,
protected timeUtils: CoreTimeUtilsProvider,
protected groupsProvider: CoreGroupsProvider,
protected injector: Injector) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -47,4 +47,13 @@ export class AddonModFolderPluginFileHandler implements CorePluginFileHandler {
// Component + Filearea + Revision
return '/mod_folder/content/0/';
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
}

View File

@ -23,6 +23,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler';
import { AddonModFolderProvider } from './folder';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch folders.
@ -41,9 +42,11 @@ export class AddonModFolderPrefetchHandler extends CoreCourseResourcePrefetchHan
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected folderProvider: AddonModFolderProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -26,6 +26,7 @@ import { CoreGroupsProvider } from '@providers/groups';
import { AddonModForumProvider } from './forum';
import { AddonModForumSyncProvider } from './sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch forums.
@ -45,12 +46,14 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
private userProvider: CoreUserProvider,
private groupsProvider: CoreGroupsProvider,
private forumProvider: AddonModForumProvider,
private syncProvider: AddonModForumSyncProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**
@ -93,7 +96,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
if (getInlineFiles && post.messageinlinefiles && post.messageinlinefiles.length) {
files = files.concat(post.messageinlinefiles);
} else if (post.message && !getInlineFiles) {
files = files.concat(this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(post.message));
files = files.concat(this.filepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects(post.message));
}
});

View File

@ -25,6 +25,7 @@ import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/acti
import { AddonModGlossaryProvider } from './glossary';
import { AddonModGlossarySyncProvider } from './sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch forums.
@ -44,11 +45,13 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected glossaryProvider: AddonModGlossaryProvider,
protected commentsProvider: CoreCommentsProvider,
protected syncProvider: AddonModGlossarySyncProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**
@ -90,7 +93,7 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH
if (getInlineFiles && entry.definitioninlinefiles && entry.definitioninlinefiles.length) {
files = files.concat(entry.definitioninlinefiles);
} else if (entry.definition && !getInlineFiles) {
files = files.concat(this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(entry.definition));
files = files.concat(this.filepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects(entry.definition));
}
});

View File

@ -54,4 +54,13 @@ export class AddonModImscpPluginFileHandler implements CorePluginFileHandler {
// Component + Filearea + Revision
return '/mod_imscp/' + args[2] + '/0/';
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
}

View File

@ -23,6 +23,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler';
import { AddonModImscpProvider } from './imscp';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch IMSCPs.
@ -41,9 +42,11 @@ export class AddonModImscpPrefetchHandler extends CoreCourseResourcePrefetchHand
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected imscpProvider: AddonModImscpProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -23,6 +23,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler';
import { AddonModLabelProvider } from './label';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch labels.
@ -43,9 +44,11 @@ export class AddonModLabelPrefetchHandler extends CoreCourseResourcePrefetchHand
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected labelProvider: AddonModLabelProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -26,6 +26,7 @@ import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/acti
import { AddonModLessonProvider } from './lesson';
import { AddonModLessonSyncProvider } from './lesson-sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch lessons.
@ -48,12 +49,14 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected modalCtrl: ModalController,
protected groupsProvider: CoreGroupsProvider,
protected lessonProvider: AddonModLessonProvider,
protected injector: Injector) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**
@ -108,7 +111,9 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan
let files = lesson.mediafiles || [];
files = files.concat(this.getIntroFilesFromInstance(module, lesson));
result = this.utils.sumFileSizes(files);
return this.pluginFileDelegate.getFilesSize(files);
}).then((res) => {
result = res;
// Get the pages to calculate the size.
return this.lessonProvider.getPages(lesson.id, password, false, false, siteId);
@ -414,7 +419,8 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan
return;
}
answerPage.answerdata.answers.forEach((answer) => {
files.push(...this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(answer[0]));
files.push(...this.filepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects(
answer[0]));
});
});

View File

@ -23,6 +23,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler';
import { AddonModLtiProvider } from './lti';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch LTIs. LTIs cannot be prefetched, but the handler will be used to invalidate some data on course PTR.
@ -41,9 +42,11 @@ export class AddonModLtiPrefetchHandler extends CoreCourseActivityPrefetchHandle
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected ltiProvider: AddonModLtiProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -47,4 +47,13 @@ export class AddonModPagePluginFileHandler implements CorePluginFileHandler {
// Component + Filearea + Revision
return '/mod_page/content/0/';
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
}

View File

@ -24,6 +24,7 @@ import { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/reso
import { AddonModPageProvider } from './page';
import { AddonModPageHelperProvider } from './helper';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch pages.
@ -43,10 +44,12 @@ export class AddonModPagePrefetchHandler extends CoreCourseResourcePrefetchHandl
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected pageProvider: AddonModPageProvider,
protected pageHelper: AddonModPageHelperProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -29,6 +29,7 @@ import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
import { AddonModQuizSyncProvider } from './quiz-sync';
import { CoreConstants } from '@core/constants';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch quizzes.
@ -50,6 +51,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected injector: Injector,
protected quizProvider: AddonModQuizProvider,
protected textUtils: CoreTextUtilsProvider,
@ -57,7 +59,8 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl
protected accessRuleDelegate: AddonModQuizAccessRuleDelegate,
protected questionHelper: CoreQuestionHelperProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**
@ -123,7 +126,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl
files = files.concat(feedback.feedbackinlinefiles);
} else if (feedback.feedbacktext && !getInlineFiles) {
files = files.concat(
this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(feedback.feedbacktext));
this.filepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects(feedback.feedbacktext));
}
}));
}

View File

@ -47,4 +47,13 @@ export class AddonModResourcePluginFileHandler implements CorePluginFileHandler
// Component + Filearea + Revision
return '/mod_resource/content/0/';
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
}

View File

@ -25,6 +25,7 @@ import { AddonModResourceProvider } from './resource';
import { AddonModResourceHelperProvider } from './helper';
import { CoreConstants } from '@core/constants';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch resources.
@ -43,10 +44,12 @@ export class AddonModResourcePrefetchHandler extends CoreCourseResourcePrefetchH
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected resourceProvider: AddonModResourceProvider,
protected resourceHelper: AddonModResourceHelperProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -47,4 +47,13 @@ export class AddonModScormPluginFileHandler implements CorePluginFileHandler {
// Component + Filearea + Revision
return '/mod_scorm/content/0/';
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
}

View File

@ -26,6 +26,7 @@ import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/acti
import { AddonModScormProvider } from './scorm';
import { AddonModScormSyncProvider } from './scorm-sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Progress event used when downloading a SCORM.
@ -67,12 +68,14 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected fileProvider: CoreFileProvider,
protected textUtils: CoreTextUtilsProvider,
protected scormProvider: AddonModScormProvider,
protected injector: Injector) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**
@ -169,11 +172,6 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand
return this.filepoolProvider.downloadUrl(siteId, packageUrl, true, this.component, scorm.coursemodule,
undefined, this.downloadProgress.bind(this, true, onProgress));
}
}).then(() => {
// Remove the destination folder to prevent having old unused files.
return this.fileProvider.removeDir(dirPath).catch(() => {
// Ignore errors, it might have failed because the folder doesn't exist.
});
}).then(() => {
// Get the ZIP file path.
return this.filepoolProvider.getFilePathByUrl(siteId, packageUrl);

View File

@ -25,6 +25,7 @@ import { AddonModSurveyProvider } from './survey';
import { AddonModSurveySyncProvider } from './sync';
import { AddonModSurveyHelperProvider } from './helper';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch surveys.
@ -46,11 +47,13 @@ export class AddonModSurveyPrefetchHandler extends CoreCourseActivityPrefetchHan
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected surveyProvider: AddonModSurveyProvider,
protected surveyHelper: AddonModSurveyHelperProvider,
protected injector: Injector) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -23,6 +23,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler';
import { AddonModUrlProvider } from './url';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch URLs. URLs cannot be prefetched, but the handler will be used to invalidate some data on course PTR.
@ -40,9 +41,11 @@ export class AddonModUrlPrefetchHandler extends CoreCourseResourcePrefetchHandle
filepoolProvider: CoreFilepoolProvider,
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider) {
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -29,6 +29,7 @@ import { CoreUserProvider } from '@core/user/providers/user';
import { AddonModWikiProvider } from './wiki';
import { AddonModWikiSyncProvider } from './wiki-sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch wikis.
@ -48,6 +49,7 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected wikiProvider: AddonModWikiProvider,
protected userProvider: CoreUserProvider,
protected textUtils: CoreTextUtilsProvider,
@ -56,7 +58,8 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl
protected gradesHelper: CoreGradesHelperProvider,
protected syncProvider: AddonModWikiSyncProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**
@ -96,7 +99,7 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl
siteId = this.sitesProvider.getCurrentSiteId();
promises.push(this.getFiles(module, courseId, single, siteId).then((files) => {
return this.utils.sumFileSizes(files);
return this.pluginFileDelegate.getFilesSize(files);
}));
promises.push(this.getAllPages(module, courseId, false, true, siteId).then((pages) => {

View File

@ -27,6 +27,7 @@ import { AddonModWorkshopProvider } from './workshop';
import { AddonModWorkshopSyncProvider } from './sync';
import { AddonModWorkshopHelperProvider } from './helper';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch workshops.
@ -47,13 +48,15 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
private groupsProvider: CoreGroupsProvider,
private userProvider: CoreUserProvider,
private workshopProvider: AddonModWorkshopProvider,
private workshopHelper: AddonModWorkshopHelperProvider,
private syncProvider: AddonModWorkshopSyncProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
}
/**

View File

@ -83,6 +83,7 @@ import { CoreBlockModule } from '@core/block/block.module';
import { CoreRatingModule } from '@core/rating/rating.module';
import { CoreTagModule } from '@core/tag/tag.module';
import { CoreFilterModule } from '@core/filter/filter.module';
import { CoreH5PModule } from '@core/h5p/h5p.module';
// Addon modules.
import { AddonBadgesModule } from '@addon/badges/badges.module';
@ -230,6 +231,7 @@ export const WP_PROVIDER: any = null;
CorePushNotificationsModule,
CoreTagModule,
CoreFilterModule,
CoreH5PModule,
AddonBadgesModule,
AddonBlogModule,
AddonCalendarModule,

View File

@ -359,6 +359,7 @@
"h261": {"type":"video/h261"},
"h263": {"type":"video/h263"},
"h264": {"type":"video/h264"},
"h5p": {"type":"application/zip","icon":"archive","string":"archive","groups":["archive"]},
"hal": {"type":"application/vnd.hal+xml"},
"hbci": {"type":"application/vnd.hbci"},
"hdf": {"type":"application/x-hdf"},

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 345 150" style="enable-background:new 0 0 345 150;" xml:space="preserve">
<g>
<path d="M325.7,14.7C317.6,6.9,305.3,3,289,3h-43.5H234v31h-66l-5.4,22.2c4.5-2.1,10.9-4.2,15.3-5.3c4.4-1.1,8.8-0.9,13.1-0.9
c14.6,0,26.5,4.5,35.6,13.3c9.1,8.8,13.6,20,13.6,33.4c0,9.4-2.3,18.5-7,27.2s-11.3,15.4-19.9,20c-3.1,1.6-6.5,3.1-10.2,4.1h42.4
H259V95h25c18.2,0,31.7-4.2,40.6-12.5s13.3-19.9,13.3-34.6C337.9,33.6,333.8,22.5,325.7,14.7z M288.7,60.6c-3.5,3-9.6,4.4-18.3,4.4
H259V33h13.2c8.4,0,14.2,1.5,17.2,4.7c3.1,3.2,4.6,6.9,4.6,11.5C294,53.9,292.2,57.6,288.7,60.6z"/>
<path d="M176.5,76.3c-7.9,0-14.7,4.6-18,11.2L119,81.9L136.8,3h-23.6H101v62H51V3H7v145h44V95h50v53h12.2h42
c-6.7-2-12.5-4.6-17.2-8.1c-4.8-3.6-8.7-7.7-11.7-12.3c-3-4.6-5.3-9.7-7.3-16.5l39.6-5.7c3.3,6.6,10.1,11.1,17.9,11.1
c11.1,0,20.1-9,20.1-20.1S187.5,76.3,176.5,76.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1550,6 +1550,95 @@
"core.group": "Group",
"core.groupsseparate": "Separate groups",
"core.groupsvisible": "Visible groups",
"core.h5p.additionallicenseinfo": "Any additional information about the license",
"core.h5p.author": "Author",
"core.h5p.authorcomments": "Author comments",
"core.h5p.authorcommentsdescription": "Comments for the editor of the content. (This text will not be published as a part of the copyright info.)",
"core.h5p.authorname": "Author's name",
"core.h5p.authorrole": "Author's role",
"core.h5p.by": "by",
"core.h5p.cancellabel": "Cancel",
"core.h5p.ccattribution": "Attribution (CC BY)",
"core.h5p.ccattributionnc": "Attribution-NonCommercial (CC BY-NC)",
"core.h5p.ccattributionncnd": "Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)",
"core.h5p.ccattributionncsa": "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)",
"core.h5p.ccattributionnd": "Attribution-NoDerivs (CC BY-ND)",
"core.h5p.ccattributionsa": "Attribution-ShareAlike (CC BY-SA)",
"core.h5p.ccpdd": "Public Domain Dedication (CC0)",
"core.h5p.changedby": "Changed by",
"core.h5p.changedescription": "Description of change",
"core.h5p.changelog": "Changelog",
"core.h5p.changeplaceholder": "Photo cropped, text changed, etc.",
"core.h5p.close": "Close",
"core.h5p.confirmdialogbody": "Please confirm that you wish to proceed. This action is not reversible.",
"core.h5p.confirmdialogheader": "Confirm action",
"core.h5p.confirmlabel": "Confirm",
"core.h5p.connectionLost": "Connection lost. Results will be stored and sent when you regain connection.",
"core.h5p.connectionReestablished": "Connection reestablished.",
"core.h5p.contentCopied": "Content is copied to the clipboard",
"core.h5p.contentchanged": "This content has changed since you last used it.",
"core.h5p.contenttype": "Content Type",
"core.h5p.copyright": "Rights of use",
"core.h5p.copyrightinfo": "Copyright information",
"core.h5p.copyrightstring": "Copyright",
"core.h5p.copyrighttitle": "View copyright information for this content.",
"core.h5p.creativecommons": "Creative Commons",
"core.h5p.date": "Date",
"core.h5p.disablefullscreen": "Disable fullscreen",
"core.h5p.download": "Download",
"core.h5p.downloadtitle": "Download this content as a H5P file.",
"core.h5p.editor": "Editor",
"core.h5p.embed": "Embed",
"core.h5p.embedtitle": "View the embed code for this content.",
"core.h5p.fullscreen": "Fullscreen",
"core.h5p.gpl": "General Public License v3",
"core.h5p.h5ptitle": "Visit H5P.org to check out more cool content.",
"core.h5p.hideadvanced": "Hide advanced",
"core.h5p.license": "License",
"core.h5p.licenseCC010": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
"core.h5p.licenseCC010U": "CC0 1.0 Universal",
"core.h5p.licenseCC10": "1.0 Generic",
"core.h5p.licenseCC20": "2.0 Generic",
"core.h5p.licenseCC25": "2.5 Generic",
"core.h5p.licenseCC30": "3.0 Unported",
"core.h5p.licenseCC40": "4.0 International",
"core.h5p.licenseGPL": "General Public License",
"core.h5p.licenseV1": "Version 1",
"core.h5p.licenseV2": "Version 2",
"core.h5p.licenseV3": "Version 3",
"core.h5p.licensee": "Licensee",
"core.h5p.licenseextras": "License Extras",
"core.h5p.licenseversion": "License version",
"core.h5p.nocopyright": "No copyright information available for this content.",
"core.h5p.offlineDialogBody": "We were unable to send information about your completion of this task. Please check your internet connection.",
"core.h5p.offlineDialogHeader": "Your connection to the server was lost",
"core.h5p.offlineDialogRetryButtonLabel": "Retry now",
"core.h5p.offlineDialogRetryMessage": "Retrying in :num....",
"core.h5p.offlineSuccessfulSubmit": "Successfully submitted results.",
"core.h5p.originator": "Originator",
"core.h5p.pd": "Public Domain",
"core.h5p.pddl": "Public Domain Dedication and Licence",
"core.h5p.pdm": "Public Domain Mark (PDM)",
"core.h5p.play": "Play H5P",
"core.h5p.resizescript": "Include this script on your website if you want dynamic sizing of the embedded content:",
"core.h5p.resubmitScores": "Attempting to submit stored results.",
"core.h5p.reuse": "Reuse",
"core.h5p.reuseContent": "Reuse Content",
"core.h5p.reuseDescription": "Reuse this content.",
"core.h5p.showadvanced": "Show advanced",
"core.h5p.showless": "Show less",
"core.h5p.showmore": "Show more",
"core.h5p.size": "Size",
"core.h5p.source": "Source",
"core.h5p.startingover": "You'll be starting over.",
"core.h5p.sublevel": "Sublevel",
"core.h5p.thumbnail": "Thumbnail",
"core.h5p.title": "Title",
"core.h5p.undisclosed": "Undisclosed",
"core.h5p.year": "Year",
"core.h5p.years": "Year(s)",
"core.h5p.yearsfrom": "Years (from)",
"core.h5p.yearsto": "Years (to)",
"core.hasdatatosync": "This {{$a}} has offline data to be synchronised.",
"core.help": "Help",
"core.hide": "Hide",

View File

@ -237,14 +237,16 @@ export class CoreDelegate {
* @return True when registered, false if already registered.
*/
registerHandler(handler: CoreDelegateHandler): boolean {
if (typeof this.handlers[handler[this.handlerNameProperty]] !== 'undefined') {
const key = handler[this.handlerNameProperty] || handler.name;
if (typeof this.handlers[key] !== 'undefined') {
this.logger.log(`Handler '${handler[this.handlerNameProperty]}' already registered`);
return false;
}
this.logger.log(`Registered handler '${handler[this.handlerNameProperty]}'`);
this.handlers[handler[this.handlerNameProperty]] = handler;
this.handlers[key] = handler;
return true;
}
@ -282,10 +284,12 @@ export class CoreDelegate {
}).then((enabled: boolean) => {
// Check that site hasn't changed since the check started.
if (this.sitesProvider.getCurrentSiteId() === siteId) {
const key = handler[this.handlerNameProperty] || handler.name;
if (enabled) {
this.enabledHandlers[handler[this.handlerNameProperty]] = handler;
this.enabledHandlers[key] = handler;
} else {
delete this.enabledHandlers[handler[this.handlerNameProperty]];
delete this.enabledHandlers[key];
}
}
}).finally(() => {

View File

@ -23,6 +23,7 @@ import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreConstants } from '@core/constants';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Component to handle a remote file. Shows the file name, icon (depending on mimetype) and a button
@ -56,10 +57,16 @@ export class CoreFileComponent implements OnInit, OnDestroy {
protected timemodified: number;
protected observer;
constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider,
private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider,
private fileHelper: CoreFileHelperProvider, private mimeUtils: CoreMimetypeUtilsProvider,
private eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider) {
constructor(private sitesProvider: CoreSitesProvider,
private utils: CoreUtilsProvider,
private domUtils: CoreDomUtilsProvider,
private filepoolProvider: CoreFilepoolProvider,
private appProvider: CoreAppProvider,
private fileHelper: CoreFileHelperProvider,
private mimeUtils: CoreMimetypeUtilsProvider,
private eventsProvider: CoreEventsProvider,
private textUtils: CoreTextUtilsProvider,
private pluginFileDelegate: CorePluginFileDelegate) {
this.onDelete = new EventEmitter();
}
@ -141,8 +148,6 @@ export class CoreFileComponent implements OnInit, OnDestroy {
e && e.preventDefault();
e && e.stopPropagation();
let promise;
if (this.isDownloading && !openAfterDownload) {
return;
}
@ -164,7 +169,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
}
if (!this.appProvider.isOnline() && (!openAfterDownload || (openAfterDownload &&
!(this.state === CoreConstants.DOWNLOADED || this.state === CoreConstants.OUTDATED)))) {
!this.fileHelper.isStateDownloaded(this.state)))) {
this.domUtils.showErrorModal('core.networkerrormsg', true);
return;
@ -177,20 +182,26 @@ export class CoreFileComponent implements OnInit, OnDestroy {
});
} else {
// File doesn't need to be opened (it's a prefetch). Show confirm modal if file size is defined and it's big.
promise = this.fileSize ? this.domUtils.confirmDownloadSize({ size: this.fileSize, total: true }) : Promise.resolve();
promise.then(() => {
// User confirmed, add the file to queue.
return this.filepoolProvider.invalidateFileByUrl(this.siteId, this.fileUrl).finally(() => {
this.isDownloading = true;
this.pluginFileDelegate.getFileSize({fileurl: this.fileUrl, filesize: this.fileSize}, this.siteId).then((size) => {
this.filepoolProvider.addToQueueByUrl(this.siteId, this.fileUrl, this.component,
this.componentId, this.timemodified, undefined, undefined, 0, this.file).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
this.calculateState();
});
const promise = size ? this.domUtils.confirmDownloadSize({ size: size, total: true }) : Promise.resolve();
return promise.then(() => {
// User confirmed, add the file to queue.
return this.filepoolProvider.invalidateFileByUrl(this.siteId, this.fileUrl).finally(() => {
this.isDownloading = true;
this.filepoolProvider.addToQueueByUrl(this.siteId, this.fileUrl, this.component,
this.componentId, this.timemodified, undefined, undefined, 0, this.file).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
this.calculateState();
});
});
}).catch(() => {
// User cancelled.
});
}).catch(() => {
// Ignore error.
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
});
}
}

View File

@ -1,4 +1,4 @@
<div [class.core-loading-container]="loading">
<div [class.core-loading-container]="loading" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}">
<iframe #iframe [hidden]="loading" class="core-iframe" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}" [src]="safeUrl"></iframe>
<span class="core-loading-spinner">
<ion-spinner *ngIf="loading"></ion-spinner>

View File

@ -1,7 +1,4 @@
ion-app.app-root core-iframe {
> div {
height: 100%;
}
iframe {
border: 0;
display: block;

View File

@ -51,7 +51,7 @@ export class CoreSitePickerComponent implements OnInit {
sites.forEach((site: any) => {
// Format the site name.
promises.push(this.filterProvider.formatText(site.siteName, {clean: true, singleLine: true, filter: false}, [],
site.getId()).catch(() => {
site.id).catch(() => {
return site.siteName;
}).then((siteName) => {
site.fullNameAndSiteName = this.translate.instant('core.fullnameandsitename',

View File

@ -30,6 +30,7 @@ import { CORE_COURSES_PROVIDERS } from '@core/courses/courses.module';
import { CORE_FILEUPLOADER_PROVIDERS } from '@core/fileuploader/fileuploader.module';
import { CORE_FILTER_PROVIDERS } from '@core/filter/filter.module';
import { CORE_GRADES_PROVIDERS } from '@core/grades/grades.module';
import { CORE_H5P_PROVIDERS } from '@core/h5p/h5p.module';
import { CORE_LOGIN_PROVIDERS } from '@core/login/login.module';
import { CORE_MAINMENU_PROVIDERS } from '@core/mainmenu/mainmenu.module';
import { CORE_QUESTION_PROVIDERS } from '@core/question/question.module';
@ -236,7 +237,7 @@ export class CoreCompileProvider {
.concat(ADDON_MOD_SURVEY_PROVIDERS).concat(ADDON_MOD_URL_PROVIDERS).concat(ADDON_MOD_WIKI_PROVIDERS)
.concat(ADDON_MOD_WORKSHOP_PROVIDERS).concat(ADDON_NOTES_PROVIDERS).concat(ADDON_NOTIFICATIONS_PROVIDERS)
.concat(CORE_PUSHNOTIFICATIONS_PROVIDERS).concat(ADDON_REMOTETHEMES_PROVIDERS).concat(CORE_BLOCK_PROVIDERS)
.concat(CORE_FILTER_PROVIDERS);
.concat(CORE_FILTER_PROVIDERS).concat(CORE_H5P_PROVIDERS);
// We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
for (const i in providers) {

View File

@ -21,6 +21,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '../providers/course';
import { CoreCourseModulePrefetchHandler } from '../providers/module-prefetch-delegate';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Base prefetch handler to be registered in CoreCourseModulePrefetchDelegate. Prefetch handlers should inherit either
@ -67,7 +68,8 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref
protected filepoolProvider: CoreFilepoolProvider,
protected sitesProvider: CoreSitesProvider,
protected domUtils: CoreDomUtilsProvider,
protected filterHelper: CoreFilterHelperProvider) { }
protected filterHelper: CoreFilterHelperProvider,
protected pluginFileDelegate: CorePluginFileDelegate) { }
/**
* Add an ongoing download to the downloadPromises list. When the promise finishes it will be removed.
@ -137,7 +139,7 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref
*/
getDownloadSize(module: any, courseId: number, single?: boolean): Promise<{ size: number, total: boolean }> {
return this.getFiles(module, courseId).then((files) => {
return this.utils.sumFileSizes(files);
return this.pluginFileDelegate.getFilesSize(files);
}).catch(() => {
return { size: -1, total: false };
});
@ -193,12 +195,12 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref
if (typeof instance.introfiles != 'undefined') {
return instance.introfiles;
} else if (instance.intro) {
return this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(instance.intro);
return this.filepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects(instance.intro);
}
}
if (module.description) {
return this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(module.description);
return this.filepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects(module.description);
}
return [];

View File

@ -438,7 +438,7 @@ export class CoreCourseHelperProvider {
sizePromise = this.prefetchDelegate.getDownloadSize(section.modules, courseId);
// Check if the section has embedded files in the description.
haveEmbeddedFiles = this.domUtils.extractDownloadableFilesFromHtml(section.summary).length > 0;
haveEmbeddedFiles = this.filepoolProvider.extractDownloadableFilesFromHtml(section.summary).length > 0;
} else {
const promises = [],
results = {
@ -454,7 +454,7 @@ export class CoreCourseHelperProvider {
}));
// Check if the section has embedded files in the description.
if (!haveEmbeddedFiles && this.domUtils.extractDownloadableFilesFromHtml(s.summary).length > 0) {
if (!haveEmbeddedFiles && this.filepoolProvider.extractDownloadableFilesFromHtml(s.summary).length > 0) {
haveEmbeddedFiles = true;
}
}
@ -1089,7 +1089,7 @@ export class CoreCourseHelperProvider {
// Get the time it was downloaded (if it was downloaded).
promises.push(this.filepoolProvider.getPackageData(siteId, component, module.id).then((data) => {
if (data && data.downloadTime && (data.status == CoreConstants.OUTDATED || data.status == CoreConstants.DOWNLOADED)) {
if (data && data.downloadTime && this.fileHelper.isStateDownloaded(data.status)) {
const now = this.timeUtils.timestamp();
moduleInfo.downloadTime = data.downloadTime;
if (now - data.downloadTime < 7 * 86400) {
@ -1449,7 +1449,7 @@ export class CoreCourseHelperProvider {
}));
// Download the files in the section description.
const introFiles = this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(section.summary),
const introFiles = this.filepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects(section.summary),
siteId = this.sitesProvider.getCurrentSiteId();
promises.push(this.filepoolProvider.addFilesToQueue(siteId, introFiles, CoreCourseProvider.COMPONENT, courseId)

View File

@ -27,6 +27,7 @@ import { CoreConstants } from '../../constants';
import { Md5 } from 'ts-md5/dist/md5';
import { Subject, BehaviorSubject, Subscription } from 'rxjs';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { CoreFileHelperProvider } from '@providers/file-helper';
/**
* Progress of downloading a list of modules.
@ -258,10 +259,15 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
}
} = {};
constructor(loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider,
private courseProvider: CoreCourseProvider, private filepoolProvider: CoreFilepoolProvider,
private timeUtils: CoreTimeUtilsProvider, private fileProvider: CoreFileProvider,
protected eventsProvider: CoreEventsProvider) {
constructor(loggerProvider: CoreLoggerProvider,
protected sitesProvider: CoreSitesProvider,
protected utils: CoreUtilsProvider,
protected courseProvider: CoreCourseProvider,
protected filepoolProvider: CoreFilepoolProvider,
protected timeUtils: CoreTimeUtilsProvider,
protected fileProvider: CoreFileProvider,
protected eventsProvider: CoreEventsProvider,
protected fileHelper: CoreFileHelperProvider) {
super('CoreCourseModulePrefetchDelegate', loggerProvider, sitesProvider, eventsProvider);
this.sitesProvider.registerSiteSchema(this.siteSchema);
@ -881,7 +887,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
const packageId = this.filepoolProvider.getPackageId(handler.component, module.id),
status = this.statusCache.getValue(packageId, 'status');
if (typeof status != 'undefined' && status != CoreConstants.DOWNLOADED && status != CoreConstants.OUTDATED) {
if (typeof status != 'undefined' && !this.fileHelper.isStateDownloaded(status)) {
// Module isn't downloaded, just return the status.
return Promise.resolve({
status: status
@ -927,7 +933,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
return this.sitesProvider.getSite(siteId).then((site) => {
// Get the status and download time of the module.
return this.getModuleStatusAndDownloadTime(module, courseId).then((data) => {
if (data.status != CoreConstants.DOWNLOADED && data.status != CoreConstants.OUTDATED) {
if (!this.fileHelper.isStateDownloaded(data.status)) {
// Not downloaded, no updates.
return {};
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Injectable, ViewContainerRef } from '@angular/core';
import { CoreFilterHandler } from './delegate';
import { CoreFilterFilter, CoreFilterFormatTextOptions } from './filter';
import { CoreSite } from '@classes/site';
@ -50,10 +50,14 @@ export class CoreFilterDefaultHandler implements CoreFilterHandler {
* @param container The HTML container to handle.
* @param filter The filter.
* @param options Options passed to the filters.
* @param viewContainerRef The ViewContainerRef where the container is.
* @param component Component.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return If async, promise resolved when done.
*/
handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string)
handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions,
viewContainerRef: ViewContainerRef, component?: string, componentId?: string | number, siteId?: string)
: void | Promise<void> {
// To be overridden.
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Injectable, ViewContainerRef } from '@angular/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
@ -48,10 +48,14 @@ export interface CoreFilterHandler extends CoreDelegateHandler {
* @param container The HTML container to handle.
* @param filter The filter.
* @param options Options passed to the filters.
* @param viewContainerRef The ViewContainerRef where the container is.
* @param component Component.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return If async, promise resolved when done.
*/
handleHtml?(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string)
handleHtml?(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions,
viewContainerRef: ViewContainerRef, component?: string, componentId?: string | number, siteId?: string)
: void | Promise<void>;
/**
@ -156,13 +160,16 @@ export class CoreFilterDelegate extends CoreDelegate {
*
* @param container The HTML container to handle.
* @param filters Filters to apply.
* @param viewContainerRef The ViewContainerRef where the container is.
* @param options Options passed to the filters.
* @param skipFilters Names of filters that shouldn't be applied.
* @param component Component.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
handleHtml(container: HTMLElement, filters: CoreFilterFilter[], options?: any, skipFilters?: string[], siteId?: string)
: Promise<any> {
handleHtml(container: HTMLElement, filters: CoreFilterFilter[], viewContainerRef?: ViewContainerRef, options?: any,
skipFilters?: string[], component?: string, componentId?: string | number, siteId?: string): Promise<any> {
// Wait for filters to be initialized.
return this.handlersInitPromise.then(() => {
@ -181,8 +188,9 @@ export class CoreFilterDelegate extends CoreDelegate {
}
promise = promise.then(() => {
return Promise.resolve(this.executeFunctionOnEnabled(filter.filter, 'handleHtml',
[container, filter, options, siteId])).catch((error) => {
[container, filter, options, viewContainerRef, component, componentId, siteId])).catch((error) => {
this.logger.error('Error handling HTML' + filter.filter, error);
});
});

View File

@ -402,7 +402,7 @@ export type CoreFilterFilter = {
*/
export type CoreFilterGetAvailableInContextResult = {
filters: CoreFilterFilter[]; // Available filters.
warning: CoreWSExternalWarning[]; // List of warnings.
warnings: CoreWSExternalWarning[]; // List of warnings.
};
/**

Binary file not shown.

View File

@ -0,0 +1,62 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet">
<metadata>
<json>
<![CDATA[
{
"fontFamily": "h5p-core-21",
"description": "Font generated by IcoMoon.",
"majorVersion": 1,
"minorVersion": 1,
"version": "Version 1.1",
"fontId": "h5p-core-21",
"psName": "h5p-core-21",
"subFamily": "Regular",
"fullName": "h5p-core-21"
}
]]>
</json>
</metadata>
<defs>
<font id="h5p-core-21" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe565;" glyph-name="arrow-down" data-tags="arrow-down" d="M234 389.669h556l-278-278z" />
<glyph unicode="&#xe566;" glyph-name="arrow-left" data-tags="arrow-left" d="M381-11.331v524l262-262z" />
<glyph unicode="&#xe58e;" glyph-name="colapse" data-tags="colapse" d="M512 447.336l256-256-60-60-196 196-196-196-60 60z" />
<glyph unicode="&#xe58f;" glyph-name="expand" data-tags="expand" d="M708 423.336l60-60-256-256-256 256 60 60 196-196z" />
<glyph unicode="&#xe600;" glyph-name="move" data-tags="move" d="M386.662 725.063h71.27v-71.27h-71.27v71.27zM566.067 725.063h71.27v-71.27h-71.27v71.27zM386.662 568.8h71.27v-71.27h-71.27v71.27zM566.067 568.8h71.27v-71.27h-71.27v71.27zM386.662 412.435h71.27v-71.27h-71.27v71.27zM566.067 412.435h71.27v-71.27h-71.27v71.27zM386.662 256.173h71.27v-71.27h-71.27v71.27zM566.067 256.173h71.27v-71.27h-71.27v71.27zM386.662 99.808h71.27v-71.27h-71.27v71.27zM566.067 99.808h71.27v-71.27h-71.27v71.27zM386.662-56.454h71.27v-71.27h-71.27v71.27zM566.067-56.454h71.27v-71.27h-71.27v71.27z" />
<glyph unicode="&#xe601;" glyph-name="check-mark" data-tags="check-mark" d="M454.299 245.924l-116.917 116.917-84.781-84.707 201.696-201.697 317.097 317.097-84.781 84.706z" />
<glyph unicode="&#xe888;" glyph-name="arrow-up-circle" data-tags="arrow-up-circle" d="M512 606.057c-148.616 0-264.722-120.75-260.077-269.367 0-125.395 88.241-232.212 208.991-255.434v213.636h-92.885c-13.933 0-13.933 9.288-9.288 18.577l139.327 171.838c4.645 9.288 13.933 9.288 23.221 4.645 0 0 4.645-4.645 4.645-4.645l139.327-171.838c9.288-9.288 4.645-18.577-9.288-18.577h-92.885v-213.636c143.972 32.51 232.212 171.838 199.703 315.808-23.221 120.75-130.039 204.347-250.789 208.991z" />
<glyph unicode="&#xe889;" glyph-name="info-circle" data-tags="info-circle" d="M512 601.601c-144.077 0-260.266-116.191-260.266-260.266s116.191-260.266 260.266-260.266 260.266 116.191 260.266 260.266v0c0 139.429-116.191 255.619-260.266 260.266zM470.171 550.478h88.305v-69.714h-88.305v69.714zM600.305 160.078h-181.257v51.123h51.123v162.666h-51.123v51.123h139.429v-218.438h46.477l-4.648-46.477z" />
<glyph unicode="&#xe88a;" glyph-name="search" data-tags="search" d="M772.098 125.51l-110.68 110.68c71.943 99.612 49.806 243.494-49.806 315.437s-243.494 44.27-315.437-55.339c-71.943-99.612-49.806-243.494 49.806-315.437 77.475-55.339 182.623-55.339 260.098 0l110.68-110.68c5.533-5.533 11.068-5.533 16.601 0 0 0 0 0 0 0l33.205 33.205c11.068 5.533 11.068 16.601 5.533 22.137 0 0 0 0 0 0zM478.795 202.985c-88.544 0-160.486 71.943-160.486 160.486s71.943 160.486 160.486 160.486 160.486-71.943 160.486-160.486-71.943-160.486-160.486-160.486v0z" />
<glyph unicode="&#xe88c;" glyph-name="fullscreen" data-tags="fullscreen" d="M368.55 490.521c5.737 5.737 0 5.737-5.737 5.737l-103.284 11.476c-5.737 5.737-11.476 0-11.476-5.737l11.476-109.021c0-5.737 5.737-5.737 5.737-5.737l103.284 103.284zM293.959 427.403l63.118-63.118c5.737-5.737 11.476-5.737 17.213 0l22.953 22.953c5.737 5.737 5.737 11.476 0 17.213l-63.118 57.379-40.166-34.429zM787.42 387.237c5.737-5.737 5.737 0 5.737 5.737l11.476 109.021c0 5.737-5.737 11.476-11.476 11.476l-109.021-11.476c-5.737 0-5.737-5.737-5.737-5.737l109.021-109.021zM724.305 461.832l-63.118-63.118c-5.737-5.737-5.737-11.476 0-17.213l22.953-22.953c5.737-5.737 11.476-5.737 17.213 0l63.118 63.118-40.166 40.166zM689.876 180.672c-5.737-5.737 0-5.737 5.737-5.737l109.021-11.476c5.737 0 11.476 5.737 11.476 11.476l-17.213 103.284c0 5.737-5.737 5.737-5.737 5.737l-103.284-103.284zM758.731 249.527l-63.118 63.118c-5.737 5.737-11.476 5.737-17.213 0l-22.953-22.953c-5.737-5.737-5.737-11.476 0-17.213l63.118-63.118 40.166 40.166zM265.269 283.956c-5.737 5.737-5.737 0-5.737-5.737l-11.476-109.021c0-5.737 5.737-11.476 11.476-11.476l109.021 11.476c5.737 0 5.737 5.737 5.737 5.737l-109.021 109.021zM334.124 209.362l63.118 63.118c5.737 5.737 5.737 11.476 0 17.213l-22.953 22.953c-5.737 5.737-11.476 5.737-17.213 0l-63.118-63.118 40.166-40.166zM161.985 593.805v-499.201h722.979v499.201h-722.979zM844.799 134.77h-636.911v413.13h636.911v-413.13z" />
<glyph unicode="&#xe88e;" glyph-name="h5p" data-tags="h5p" d="M934.072 489.192c-22.319 16.738-50.216 27.897-89.273 27.897h-139.487v-66.954h-156.225l-11.159-55.795c11.159 5.579 27.897 11.159 39.057 11.159s22.319 0 33.476 0c33.476 0 66.954-11.159 89.273-33.476s33.476-50.216 33.476-83.692c0-22.319-5.579-44.635-16.738-66.954s-27.897-39.057-50.216-50.216c-5.579-5.579-16.738 0-22.319-11.159h117.17v133.908h66.954c44.635 0 78.113 11.159 100.43 27.897 22.319 22.319 33.476 50.216 33.476 83.692 0 39.057-11.159 66.954-27.897 83.692v0zM839.221 377.603c-11.159-5.579-22.319-11.159-44.635-11.159h-33.476v83.692h39.057c22.319 0 33.476-5.579 44.635-11.159 5.579-5.579 11.159-16.738 11.159-27.897 0-16.738-5.579-27.897-16.738-33.476v0zM565.826 338.546c-16.738 0-33.476-11.159-44.635-27.897l-94.851 16.738 44.635 195.281h-94.851v-150.646h-117.17v150.646h-111.589v-362.667h111.589v133.908h117.17v-133.908h139.487c-16.738 11.159-33.476 11.159-44.635 22.319s-22.319 22.319-27.897 33.476c-5.579 11.159-11.159 22.319-16.738 39.057l94.851 16.738c5.579-16.738 22.319-27.897 44.635-27.897 27.897 0 50.216 22.319 50.216 50.216 0 22.319-22.319 44.635-50.216 44.635v0z" />
<glyph unicode="&#xe88f;" glyph-name="rights-of-use" data-tags="rights-of-use" d="M899.611 329.519c0-5.907 0-5.907 0-5.907-23.631-23.631-47.261-35.448-76.799-41.355-11.813 0-23.631-5.907-35.448-5.907s-17.724 0-29.537 0c0 0-5.907 0-5.907 5.907-64.985 59.079-135.877 118.153-200.863 183.139 0 0-5.907 0-5.907 0-23.631-5.907-47.261-11.813-70.892-17.724s-53.168 0-76.799 11.813c-17.724 11.813-23.631 17.724-29.537 35.448-5.907 5.907 0 23.631 11.813 23.631 41.355 11.813 88.616 29.537 129.971 47.261 11.813 5.907 29.537 5.907 41.355 5.907 5.907 0 11.813-5.907 11.813-5.907 41.355-17.724 82.709-29.537 124.060-47.261 0 0 5.907 0 5.907 0 29.537 5.907 64.985 17.724 94.523 23.631 5.907 0 5.907 0 5.907 0l106.34-212.676zM291.12 335.429c17.724 11.813 35.448 5.907 53.168-11.813 11.813-11.813 11.813-29.537 5.907-47.261 17.724 5.907 35.448-5.907 41.355-17.724 11.813-17.724 5.907-35.448-5.907-47.261 5.907 0 11.813 0 17.724 0 11.813-5.907 23.631-11.813 29.537-29.537s0-29.537-5.907-35.448c-5.907-5.907-11.813-11.813-17.724-17.724s-11.813-11.813-17.724-17.724-35.448-17.724-53.168 0c-29.537 29.537-53.168 64.985-82.709 94.523-17.724 23.631-35.448 41.355-47.261 64.985-5.907 11.813-11.813 17.724-11.813 29.537 0 5.907 0 17.724 5.907 23.631 11.813 11.813 17.724 17.724 29.537 29.537 17.724 17.724 47.261 11.813 64.985-5.907-5.907 0-5.907-5.907-5.907-11.813v0zM438.811 128.66l29.537-29.537c17.724-17.724 47.261-11.813 59.079 5.907l-5.907 5.907c-23.631 23.631-47.261 47.261-70.892 70.892-5.907 5.907-5.907 5.907-5.907 11.813s5.907 5.907 11.813 11.813c5.907 0 11.813 0 11.813-5.907 11.813-11.813 29.537-29.537 47.261-47.261 11.813-11.813 29.537-29.537 47.261-47.261 5.907-11.813 17.724-11.813 29.537-11.813 11.813 5.907 23.631 11.813 29.537 23.631 0 5.907 0 5.907 0 5.907-41.355 41.355-88.616 82.709-129.971 129.971-5.907 5.907-5.907 5.907-5.907 11.813 0 11.813 11.813 11.813 23.631 5.907 0 0 5.907 0 5.907-5.907 41.355-41.355 88.616-88.616 129.971-129.971 5.907-5.907 5.907-5.907 5.907-5.907 17.724 0 35.448 17.724 35.448 35.448 0 5.907 0 5.907 0 5.907-47.261 47.261-100.429 100.429-147.691 147.691-5.907 5.907-5.907 5.907-5.907 11.813s5.907 11.813 5.907 11.813c5.907 0 11.813 0 11.813-5.907 5.907-5.907 5.907-5.907 11.813-11.813 35.448-35.448 70.892-70.892 106.34-106.34 11.813-11.813 23.631-23.631 29.537-29.537 0 0 5.907-5.907 5.907 0 23.631 5.907 35.448 29.537 29.537 53.168h35.448c0 0 0 0 0 0 0-5.907 0-17.724 0-23.631-5.907-29.537-23.631-47.261-53.168-59.079 0 0-5.907 0-5.907-5.907-11.813-29.537-35.448-53.168-64.985-53.168-5.907 0-5.907 0-5.907-5.907-17.724-35.448-59.079-47.261-88.616-35.448-5.907 0-11.813 5.907-11.813 5.907-5.907-5.907-11.813-11.813-23.631-17.724-29.537-11.813-59.079-5.907-76.799 11.813-11.813 11.813-17.724 17.724-29.537 29.537 17.724 23.631 23.631 29.537 29.537 41.355v0 0zM273.396 642.628c29.537-11.813 64.985-23.631 94.523-29.537 35.448-11.813 64.985-23.631 100.429-29.537 0 0 0 0 5.907 0-17.724-5.907-35.448-11.813-47.261-17.724 0 0-5.907 0-5.907 0-47.261 11.813-94.523 23.631-135.877 41.355-5.907 0-5.907 0-5.907 0l-76.799-183.139c0-11.813 5.907-17.724 11.813-23.631s5.907-5.907 5.907-11.813c-5.907-5.907-11.813-17.724-23.631-23.631-17.724 17.724-29.537 35.448-29.537 64.985l88.616 212.676c-5.907-11.813 5.907 5.907 17.724 0v0z" />
<glyph unicode="&#xe890;" glyph-name="delete-circle" data-tags="delete-circle" d="M512 601.601c-147.107 0-260.266-118.817-260.266-260.266s118.817-260.266 260.266-260.266 260.266 118.817 260.266 260.266-113.158 260.266-260.266 260.266zM653.449 262.123c5.659-5.659 5.659-16.973 0-28.29l-33.949-33.949c-5.659-5.659-16.973-5.659-28.29 0l-79.212 79.212-79.212-79.212c-5.659-5.659-16.973-5.659-28.29 0l-33.949 33.949c-5.659 5.659-5.659 16.973 0 28.29l84.871 79.212-79.212 79.212c-5.659 5.659-5.659 16.973 0 28.29l33.949 33.949c5.659 5.659 16.973 5.659 28.29 0l73.554-84.871 79.212 79.212c5.659 5.659 16.973 5.659 28.29 0l33.949-33.949c5.659-5.659 5.659-16.973 0-28.29l-79.212-73.554 79.212-79.212z" />
<glyph unicode="&#xe891;" glyph-name="window" data-tags="window" d="M203.936 461.136c-5.704-5.704 0-5.704 5.704-5.704l108.394-11.41c5.704 0 11.41 5.704 11.41 11.41l-17.114 102.687c0 5.704-5.704 5.704-5.704 5.704l-102.687-102.687zM272.395 523.891l-62.752 62.752c-5.704 5.704-11.41 5.704-17.114 0l-17.114-22.821c-5.704-5.704-5.704-11.41 0-17.114l62.752-62.752 34.228 39.935zM751.605 558.119c-5.704 5.704-5.704 0-5.704-5.704l-11.41-108.394c0-5.704 5.704-11.41 11.41-11.41l108.394 11.41c5.704 0 5.704 5.704 5.704 5.704l-108.394 108.394zM814.357 483.957l62.752 62.752c5.704 5.704 5.704 11.41 0 17.114l-22.821 22.821c-5.704 5.704-11.41 5.704-17.114 0l-62.752-62.752 39.935-39.935zM848.588 221.534c5.704 5.704 0 5.704-5.704 5.704l-102.687 17.114c-5.704 0-11.41-5.704-11.41-11.41l11.41-108.394c0-5.704 5.704-5.704 5.704-5.704l102.687 102.687zM780.129 158.779l62.752-62.752c5.704-5.704 11.41-5.704 17.114 0l22.821 22.821c5.704 5.704 5.704 11.41 0 17.114l-62.752 62.752-39.935-39.935zM300.919 124.551c5.704-5.704 5.704 0 5.704 5.704l11.41 108.394c0 5.704-5.704 11.41-11.41 11.41l-108.394-11.41c-5.704 0-5.704-5.704-5.704-5.704l108.394-108.394zM238.167 193.010l-62.752-62.752c-5.704-5.704-5.704-11.41 0-17.114l22.821-22.821c5.704-5.704 11.41-5.704 17.114 0l62.752 62.752-39.935 39.935zM352.264 466.843v-239.605h347.998v239.605h-347.998zM654.622 267.172h-262.424v154.032h262.424v-154.032z" />
<glyph unicode="&#xe892;" glyph-name="code" data-tags="code" d="M449.641 235.325c6.235-6.235 6.235-12.472 6.235-18.707v-62.359c0-6.235-6.235-6.235-6.235-6.235l-230.728 155.897c-6.235 6.235-6.235 12.472-6.235 18.707v49.886c0 6.235 6.235 12.472 6.235 18.707l230.728 155.897c6.235 6.235 6.235 0 6.235-6.235v-62.359c0-6.235-6.235-12.472-6.235-18.707l-162.134-112.245c-6.235-6.235-6.235-6.235 0-12.472l162.134-99.776zM736.493 341.335c6.235 6.235 6.235 6.235 0 12.472l-155.897 112.245c-6.235 6.235-6.235 12.472-6.235 18.707v62.359c0 6.235 6.235 6.235 6.235 6.235l230.728-155.897c6.235-6.235 6.235-12.472 6.235-18.707v-49.886c0-6.235-6.235-12.472-6.235-18.707l-230.728-155.897c-6.235-6.235-6.235 0-6.235 6.235v62.359c0 6.235 6.235 12.472 6.235 18.707l155.897 99.776z" />
<glyph unicode="&#xe893;" glyph-name="download" data-tags="download" d="M358.941 435.525c-11.773 0-17.66-5.887-5.887-17.66l153.059-188.382c5.887-11.773 23.547-11.773 29.433 0l153.059 188.382c5.887 11.773 5.887 17.66-5.887 17.66h-323.782zM576.756 423.751v135.399c0 11.773-11.773 23.547-23.547 23.547h-70.643c-11.773 0-23.547-11.773-23.547-23.547v-141.286h117.739zM653.286 288.352c-5.887 0-17.66-5.887-23.547-11.773l-76.53-94.19c-5.887-5.887-17.66-17.66-23.547-23.547 0 0-5.887-5.887-11.773-5.887s-17.66 11.773-17.66 11.773c-5.887 5.887-17.66 17.66-23.547 23.547l-76.53 94.19c-5.887 5.887-17.66 11.773-23.547 11.773h-123.626c-5.887 0-17.66-5.887-17.66-17.66v-141.286c0-5.887 5.887-17.66 17.66-17.66h529.824c5.887 0 17.66 5.887 17.66 17.66v141.286c0 5.887-5.887 17.66-17.66 17.66l-129.513-5.887zM305.958 176.502c-17.66 0-29.433 11.773-29.433 29.433s11.773 29.433 29.433 29.433c17.66 0 29.433-11.773 29.433-29.433s-11.773-29.433-29.433-29.433v0z" />
<glyph unicode="&#xe894;" glyph-name="delete" data-tags="delete" d="M620.266 341.335l134.045 134.045c10.311 10.311 10.311 30.934 0 41.245l-61.866 61.866c-10.311 10.311-30.934 10.311-41.245 0l-134.045-134.045-134.045 134.045c-10.311 10.311-30.934 10.311-41.245 0l-61.866-61.866c-10.311-10.311-10.311-30.934 0-41.245l134.045-134.045-134.045-134.045c-10.311-10.311-10.311-30.934 0-41.245l61.866-61.866c10.311-10.311 30.934-10.311 41.245 0l134.045 134.045 134.045-134.045c10.311-10.311 30.934-10.311 41.245 0l61.866 61.866c10.311 10.311 10.311 30.934 0 41.245l-134.045 134.045z" />
<glyph unicode="&#xe900;" glyph-name="edit-image" data-tags="edit-image" d="M300.237 621.639c69.018 23.142 133.325 14.234 189.133-33.28 56.627-48.128 77.619-110.592 63.181-183.808-2.355-12.186 0.307-19.456 8.704-27.853 93.901-93.389 156.774-156.467 250.47-250.163 5.427-5.427 10.957-10.854 15.667-16.896 39.424-50.278 16.794-124.006-44.237-142.029-36.966-10.957-68.403 0-95.334 27.034-95.642 96.051-160.973 160.973-256.614 257.024-6.963 6.963-12.8 8.909-22.63 6.758-117.76-26.317-229.171 60.826-231.731 181.453-0.614 26.419 3.584 52.326 15.974 77.926 34.816-34.816 68.506-67.789 101.274-101.786 10.445-10.752 20.992-15.36 36.045-15.36 14.643 0 25.19 3.891 34.816 14.848 10.752 12.39 23.040 23.347 34.611 35.021 14.336 14.438 14.336 46.080-0.205 60.518-35.123 35.226-70.349 70.349-106.598 106.496 3.891 2.253 5.632 3.482 7.475 4.096zM703.386 63.559c-0.41-24.269 20.685-45.466 44.851-45.158 23.757 0.41 44.032 20.992 43.93 44.544-0.102 23.757-20.275 44.032-44.237 44.134-23.859 0.307-44.237-19.661-44.544-43.52z" />
<glyph unicode="&#xe901;" glyph-name="hourglass" data-tags="hourglass" d="M733.286-10.579c-147.763 0-295.526 0-443.29 0 0 2.048 0.102 4.096 0 6.144-0.307 13.824-1.024 32.666-0.922 46.49 0.41 39.731 6.861 78.131 19.046 115.2 17.203 52.224 43.725 96.256 81.306 130.355 4.506 4.096 9.216 7.885 13.722 11.776-0.205 0.717-0.307 1.126-0.41 1.229-1.331 1.229-2.765 2.355-4.198 3.584-28.058 22.63-50.688 51.405-68.403 85.606-30.618 59.085-43.52 123.597-41.165 192.614 0.205 7.168 0.614 18.33 0.922 25.498 147.763 0 295.526 0 443.29 0 0.205-1.331 0.512-2.662 0.614-3.994 2.662-36.966 1.229-77.722-5.939-113.869-14.336-72.909-44.544-133.837-95.027-179.405-4.096-3.686-8.294-7.066-12.39-10.547 0.205-0.717 0.307-1.126 0.512-1.331 0.819-0.717 1.638-1.434 2.458-2.15 42.189-33.894 71.68-79.872 90.931-135.782 11.776-34.202 18.637-69.837 20.070-106.701 0.819-19.763-0.614-44.851-1.126-64.717zM687.309 32.634c0 6.554 0.205 12.493 0 18.432-1.331 37.581-7.27 74.138-19.866 108.749-17.92 49.562-45.568 88.269-88.678 108.646-2.458 1.126-2.97 3.072-2.97 5.837 0.102 16.691 0.102 33.485 0 50.176 0 3.994 1.331 5.427 4.096 6.963 9.114 5.325 18.432 10.24 26.829 16.896 29.696 23.552 49.152 56.934 62.362 95.744 10.342 30.413 15.77 62.259 17.818 94.822 0.614 9.114 0.102 18.227 0.102 27.546-116.634 0-233.574 0-351.027 0 0.307-8.704 0.614-16.998 1.024-25.395 1.946-37.274 8.499-73.216 21.504-107.213 18.125-47.104 45.261-83.558 86.528-103.117 2.253-1.024 2.867-2.662 2.867-5.427-0.102-17.203-0.102-34.509 0-51.712 0-3.072-1.024-4.506-3.277-5.632-5.632-2.867-11.366-5.734-16.691-9.216-34.304-22.733-56.832-57.754-71.987-100.147-12.493-35.123-18.125-71.987-19.661-109.875-0.205-5.325 0-10.752 0-16.282 117.35 0.205 234.189 0.205 351.027 0.205zM410.214 451.962c68.096 0 135.373 0 203.674 0-3.789-6.554-7.168-12.595-10.752-18.227-10.957-16.998-24.269-30.618-39.731-41.472s-27.75-25.293-32.768-46.592c-1.638-6.963-2.765-14.336-2.867-21.606-0.307-17.203-0.205-34.406 0.307-51.712 0.717-28.058 12.493-48.947 32.154-62.566 43.008-30.003 65.843-75.878 75.776-132.506 0.307-1.638 0.307-3.379 0.614-5.53-83.149 0-166.093 0-249.242 0 2.662 20.685 7.885 40.346 15.462 59.085 13.312 32.973 32.768 59.187 59.494 77.722 16.589 11.469 28.365 27.955 32.358 50.995 0.819 4.813 1.331 9.83 1.434 14.746 0.102 18.637 0.614 37.274-0.205 55.808-1.126 24.678-11.981 43.213-28.57 57.139-9.216 7.782-18.944 14.746-27.648 23.142-11.981 11.162-21.197 25.395-29.491 41.574z" />
<glyph unicode="&#xe902;" glyph-name="plus-icon" data-tags="plus-icon" d="M768 285.015c0-19.323-15.664-34.987-34.987-34.987h-151.040v-151.467c0-19.323-15.664-34.987-34.987-34.987h-69.547c-19.323 0-34.987 15.664-34.987 34.987v151.467h-151.467c-19.323 0-34.987 15.664-34.987 34.987v69.547c0 19.323 15.664 34.987 34.987 34.987h151.467v151.467c0 19.323 15.664 34.987 34.987 34.987h69.547c19.323 0 34.987-15.664 34.987-34.987v-151.467h151.467c19.323 0 34.987-15.664 34.987-34.987z" />
<glyph unicode="&#xe903;" glyph-name="video-upload-icon" data-tags="video-upload-icon" d="M384 328.535v-128c0-21.333 21.333-42.667 42.667-42.667h128c21.333 0 42.667 17.067 42.667 42.667v128c0 21.333-17.067 42.667-42.667 42.667h-128c-21.333 0-42.667-17.067-42.667-42.667zM785.067 499.201l-102.4 106.667c-12.8 12.8-38.4 21.333-55.467 21.333h-140.8l21.333-42.667h89.6v-136.533c0-17.067 12.8-29.867 29.867-29.867h140.8v-341.333h-426.667v328.533h-42.667v-341.333c0-17.067 12.8-29.867 29.867-29.867h448c17.067 0 29.867 12.8 29.867 29.867v384c4.267 17.067-8.533 38.4-21.333 51.2zM640 456.535v123.733c4.267 0 12.8-4.267 12.8-8.533l102.4-102.4c4.267-4.267 4.267-8.533 8.533-12.8h-123.733zM725.333 166.401v196.267c0 4.267-4.267 8.533-8.533 8.533s-8.533 0-12.8-4.267l-89.6-89.6v-29.867l89.6-89.6c8.533-4.267 8.533 0 12.8 0 4.267 4.267 8.533 4.267 8.533 8.533zM349.867 473.601v136.533l59.733-59.733c8.533-8.533 17.067-4.267 25.6 0l12.8 12.8c8.533 8.533 8.533 17.067 0 25.6l-115.2 106.667c-8.533 8.533-17.067 8.533-21.333 0l-115.2-110.933c-4.267-8.533-4.267-17.067 0-25.6l12.8-12.8c8.533-8.533 17.067-8.533 21.333 0l59.733 59.733v-136.533c0-8.533 8.533-17.067 17.067-17.067h25.6c0 0 17.067 8.533 17.067 21.333z" />
<glyph unicode="&#xe904;" glyph-name="play-icon" data-tags="play-icon" d="M392.533 597.335c81.067 46.933 187.733 46.933 273.067 0 42.667-25.6 72.533-55.467 98.133-98.133 72.533-128 29.867-294.4-98.133-371.2-128-72.533-294.4-29.867-371.2 98.133-46.933 81.067-46.933 187.733 0 273.067 21.333 42.667 55.467 72.533 98.133 98.133zM661.333 345.601c12.8 8.533 12.8 29.867 0 38.4l-192 110.933c-8.533 4.267-12.8 4.267-21.333 0s-12.8-12.8-12.8-21.333v-226.133c0-8.533 4.267-17.067 12.8-21.333s17.067-4.267 21.333 0l192 119.467z" />
<glyph unicode="&#xe905;" glyph-name="copy" data-tags="copy" d="M722.133 561.201h-247.733c-65.867 0-119.2-53.333-119.2-119.2v-288.533c0-65.867 53.333-119.2 119.2-119.2h247.867c65.867 0 119.2 53.333 119.2 119.2v288.533c-0.267 65.867-53.467 119.2-119.333 119.2zM778.533 156.534c0-31.333-25.067-56.4-56.4-56.4h-247.867c-31.333 0-56.4 25.067-56.4 56.4v285.467c0 31.333 25.067 56.4 56.4 56.4h247.733c31.333 0 56.4-25.067 56.4-56.4v-285.467zM245.2 326.001v288.533c0 31.333 25.067 56.4 56.4 56.4h247.733c31.333 0 56.4-25.067 56.4-56.4v-18.8h62.667v18.8c0 65.867-53.333 119.2-119.2 119.2h-247.467c-65.867 0-119.2-53.333-119.2-119.2v-288.533c0-65.867 53.333-119.2 119.2-119.2h18.8v62.667h-18.8c-31.467-3.067-56.533 22-56.533 56.533zM681.2 360.534h-163.067c-18.8 0-31.333 12.533-31.333 31.333s12.533 31.333 31.333 31.333h163.067c18.8 0 31.333-12.533 31.333-31.333s-15.6-31.333-31.333-31.333zM681.2 266.401h-163.067c-18.8 0-31.333 12.533-31.333 31.333s12.533 31.333 31.333 31.333h163.067c18.8 0 31.333-12.533 31.333-31.333s-15.6-31.333-31.333-31.333zM681.2 172.268h-163.067c-18.8 0-31.333 12.533-31.333 31.333s12.533 31.333 31.333 31.333h163.067c18.8 0 31.333-12.533 31.333-31.333s-15.6-31.333-31.333-31.333z" />
<glyph unicode="&#xe906;" glyph-name="examples-icon" data-tags="examples-icon" d="M213.333 166.402c89.6 38.4 183.467 68.267 273.067 17.067v281.6c-68.267 46.933-157.867 55.467-234.667 12.8l-38.4-311.467zM810.667 166.402l-42.667 315.733c-72.533 38.4-166.4 34.133-234.667-17.067v-285.867c93.867 51.2 187.733 21.333 277.333-12.8zM832 520.535c-51.2 29.867-110.933 46.933-170.667 55.467-51.2 0-102.4-8.533-149.333-29.867-46.933 21.333-98.133 29.867-149.333 29.867-59.733-4.267-119.467-25.6-170.667-55.467l-64-452.267c0 0 29.867-17.067 110.933 21.333 46.933 25.6 102.4 34.133 157.867 25.6 42.667-4.267 85.333-21.333 115.2-55.467v0c29.867 34.133 72.533 51.2 115.2 55.467 55.467 4.267 106.667-4.267 157.867-29.867 81.067-38.4 110.933-21.333 110.933-21.333l-64 456.533zM793.6 115.202c-42.667 21.333-89.6 34.133-140.8 34.133-8.533 0-21.333 0-29.867 0-42.667-4.267-81.067-17.067-115.2-42.667-34.133 25.6-72.533 38.4-115.2 42.667-12.8 0-21.333 0-34.133 0-46.933 0-93.867-8.533-136.533-29.867-21.333-12.8-46.933-21.333-72.533-25.6l64 413.867c46.933 25.6 98.133 38.4 149.333 42.667 46.933 0 93.867-8.533 136.533-25.6l12.8-8.533 12.8 4.267c42.667 17.067 89.6 25.6 136.533 25.6 51.2-4.267 102.4-17.067 149.333-42.667l59.733-418.133c-29.867 8.533-51.2 17.067-76.8 29.867z" />
<glyph unicode="&#xe907;" glyph-name="tutorials-icon" data-tags="tutorials-icon" d="M887.467 430.935l-375.467-110.933h-4.267l-217.6 68.267c-21.333-25.6-34.133-59.733-34.133-98.133 21.333-12.8 25.6-38.4 12.8-59.733-4.267-4.267-8.533-8.533-12.8-12.8l17.067-145.067c0-4.267 0-4.267-4.267-8.533 0 0 0 0-4.267 0h-64c-4.267 0-4.267 0-8.533 4.267 0 4.267-4.267 4.267-4.267 8.533l17.067 145.067c-12.8 8.533-17.067 21.333-17.067 34.133 0 17.067 8.533 29.867 21.333 38.4 0 38.4 12.8 76.8 34.133 110.933l-106.667 29.867c-8.533 4.267-8.533 8.533-8.533 17.067 0 4.267 4.267 4.267 4.267 4.267l375.467 119.467h4.267l375.467-123.733c4.267 0 8.533-4.267 8.533-8.533s-4.267-8.533-8.533-12.8zM725.333 234.669c4.267-46.933-93.867-85.333-213.333-85.333s-213.333 38.4-213.333 85.333l4.267 106.667 192-64c4.267 0 12.8 0 17.067 0s12.8 0 17.067 4.267l192 59.733 4.267-106.667z" />
<glyph unicode="&#xe908;" glyph-name="info-important-description" data-tags="info-important-description" d="M512 697.368c-188.5 0-341.3-152.8-341.3-341.3s152.8-341.4 341.3-341.4 341.3 152.8 341.3 341.3-152.8 341.4-341.3 341.4v0zM512 43.268c-172.7 0-312.7 140-312.7 312.7s140 312.7 312.7 312.7c172.7 0 312.7-140 312.7-312.7-0.2-172.6-140.1-312.5-312.7-312.7v0zM512 605.568c-137.9 0-249.6-111.8-249.6-249.6s111.7-249.6 249.6-249.6 249.6 111.8 249.6 249.6-111.8 249.6-249.6 249.6v0z" />
<glyph unicode="&#xe909;" glyph-name="icon-info" data-tags="icon-info" d="M467.2 459.522h87.467c0.028 0 0.062 0 0.095 0 6.056 0 11.499 2.629 15.248 6.808 3.979 4.15 6.419 9.769 6.419 15.957 0 0.097-0.001 0.194-0.002 0.29v70.385c0.001 0.082 0.002 0.179 0.002 0.276 0 6.188-2.44 11.806-6.409 15.946-3.759 4.19-9.201 6.819-15.257 6.819-0.033 0-0.067 0-0.1 0h-87.462c-0.028 0-0.062 0-0.095 0-6.056 0-11.499-2.629-15.248-6.808-3.979-4.15-6.419-9.769-6.419-15.957 0-0.097 0.001-0.194 0.002-0.29v-69.959c-0.001-0.082-0.002-0.179-0.002-0.276 0-6.188 2.44-11.806 6.409-15.946 3.715-4.373 9.2-7.159 15.338-7.245zM597.333 156.589h-22.187v209.92c0.001 0.082 0.002 0.179 0.002 0.276 0 6.188-2.44 11.806-6.409 15.946-3.759 4.19-9.201 6.819-15.257 6.819-0.033 0-0.067 0-0.1 0h-130.128c-0.028 0-0.062 0-0.095 0-6.056 0-11.499-2.629-15.248-6.808-3.979-4.15-6.419-9.769-6.419-15.957 0-0.097 0.001-0.194 0.002-0.29v-46.492c-0.001-0.082-0.002-0.179-0.002-0.276 0-6.188 2.44-11.806 6.409-15.946 3.759-4.19 9.201-6.819 15.257-6.819 0.033 0 0.067 0 0.1 0h22.182v-139.947h-22.187c-0.028 0-0.062 0-0.095 0-6.056 0-11.499-2.629-15.248-6.808-3.979-4.15-6.419-9.769-6.419-15.957 0-0.097 0.001-0.194 0.002-0.29v-46.492c-0.001-0.082-0.002-0.179-0.002-0.276 0-6.188 2.44-11.806 6.409-15.946 3.759-4.19 9.201-6.819 15.257-6.819 0.033 0 0.067 0 0.1 0h174.075c0.028 0 0.062 0 0.095 0 6.056 0 11.499 2.629 15.248 6.808 3.979 4.15 6.419 9.769 6.419 15.957 0 0.097-0.001 0.194-0.002 0.29v46.065c0.043 0.527 0.067 1.141 0.067 1.761 0 5.302-1.791 10.185-4.8 14.079-3.742 4.424-9.36 7.247-15.636 7.247-0.489 0-0.975-0.017-1.456-0.051z" />
<glyph unicode="&#xe90a;" glyph-name="paste" data-tags="paste" d="M394.402 702.4h-75.333c-65.867 0-119.2-53.333-119.2-119.2v-288.533c0-56.4 37.6-100.4 87.867-116v69.067c-15.733 9.467-25.067 25.067-25.067 47.067v288.4c0 31.333 25.067 56.4 56.4 56.4h131.733c0 0 0 0 3.2 3.2v0c0 31.333-28.267 59.6-59.6 59.6zM704.802 592.533c0 0-28.267 0-40.8 0-12.533 34.533-43.867 59.6-84.667 59.6s-69.067-25.067-81.6-59.6c-12.533 0-40.8 0-40.8 0-65.867 0-119.2-53.333-119.2-119.2v-288.533c0-65.867 53.333-119.2 119.2-119.2h247.867c65.867 0 119.2 53.333 119.2 119.2v285.467c3.2 65.867-53.2 122.267-119.2 122.267zM582.535 605.2c22 0 40.8-18.8 40.8-40.8s-18.8-40.8-40.8-40.8c-22 0-40.8 18.8-40.8 40.8s15.733 40.8 40.8 40.8zM764.402 181.733c0-31.333-25.067-56.4-56.4-56.4h-250.933c-31.333 0-56.4 25.067-56.4 56.4v288.533c0 18.8 9.467 37.6 25.067 47.067v0c0-43.867 34.533-78.4 78.4-78.4h160c43.867 0 78.4 34.533 78.4 78.4v0c12.533-9.467 22-28.267 22-47.067v-288.533z" />
<glyph unicode="&#xe90b;" glyph-name="reuse" data-tags="reuse" d="M734.974 624.751c-54.605 61.619-134.123 100.573-222.977 100.573-164.936 0-298.661-133.724-298.661-298.661h74.667c0 123.721 100.272 223.993 223.993 223.993 68.214 0 128.747-30.96 169.766-79.119l-70.213-70.213h199.109v199.109l-75.689-75.689zM511.999 202.671c-68.214 0-128.747 30.96-169.766 79.119l70.213 70.213h-199.109v-199.109l75.689 75.689c54.605-61.619 134.123-100.573 222.977-100.573 164.936 0 298.661 133.724 298.661 298.661h-74.667c0-123.721-100.272-223.993-223.993-223.993z" />
<glyph unicode="&#xe90c;" glyph-name="info-outlined" data-tags="info-outlined" d="M467.199 181.336h89.599v268.8h-89.599v-268.8zM512 853.335c-247.296 0-448.001-200.705-448.001-448.001s200.705-448.001 448.001-448.001 448.001 200.705 448.001 448.001-200.705 448.001-448.001 448.001zM512 46.936c-197.568 0-358.398 160.83-358.398 358.398s160.83 358.398 358.398 358.398 358.398-160.83 358.398-358.398-160.83-358.398-358.398-358.398zM467.199 539.734h89.599v89.599h-89.599v-89.599z" />
<glyph unicode="&#xe90d;" glyph-name="spinner" data-tags="spinner" d="M600 808.001c0-48.602-39.398-88-88-88s-88 39.398-88 88 39.398 88 88 88 88-39.398 88-88zM512 133.333c-48.602 0-88-39.398-88-88s39.398-88 88-88 88 39.398 88 88-39.398 88-88 88zM893.334 514.667c-48.602 0-88-39.398-88-88s39.398-88 88-88 88 39.398 88 88-39.398 88-88 88zM218.666 426.667c0 48.602-39.398 88-88 88s-88-39.398-88-88 39.398-88 88-88 88 39.398 88 88zM242.357 245.024c-48.602 0-88-39.398-88-88s39.398-88 88-88 88 39.398 88 88c0 48.6-39.4 88-88 88zM781.643 245.024c-48.602 0-88-39.398-88-88s39.398-88 88-88 88 39.398 88 88c0 48.6-39.398 88-88 88zM242.357 784.31c-48.602 0-88-39.398-88-88s39.398-88 88-88 88 39.398 88 88-39.4 88-88 88z" />
<glyph unicode="&#xe90e;" glyph-name="copy-enabled" data-tags="copy-enabled" d="M614.525 809.292h-317.672c-74.968 0-135.9-60.931-135.9-135.9v-370.388c0-43.817 20.88-82.842 53.231-107.83v70.346c-4.45 11.297-7.016 23.962-7.016 37.484v370.388c0 49.292 40.224 89.516 89.516 89.516h318.014c15.232 0 29.611-3.766 42.107-10.613h68.294c-24.646 34.402-65.039 56.997-110.57 56.997zM674.431 267.403h-209.327c-14.721 0-23.105-8.388-23.105-23.105s8.388-23.105 23.105-23.105h209.327c11.124 0 23.105 8.899 23.105 23.105 0 14.721-8.388 23.105-23.105 23.105zM674.431 388.244h-209.327c-14.721 0-23.105-8.388-23.105-23.105s8.388-23.105 23.105-23.105h209.327c11.124 0 23.105 8.899 23.105 23.105 0 14.721-8.388 23.105-23.105 23.105zM726.978 686.23h-318.014c-74.968 0-135.9-60.931-135.9-135.9v-370.388c0-74.968 60.931-135.9 135.9-135.9v0h318.183c74.968 0 135.9 60.931 135.9 135.9v370.388c-0.342 74.968-61.273 135.9-136.073 135.9zM816.494 183.878c0-49.292-40.224-89.516-89.516-89.516h-318.183c-49.292 0-89.516 40.224-89.516 89.516v366.449c0 49.292 40.224 89.516 89.516 89.516h318.014c49.292 0 89.516-40.224 89.516-89.516v-349.334h0.173v-17.115zM674.431 509.081h-209.327c-14.721 0-23.105-8.388-23.105-23.105s8.388-23.105 23.105-23.105h209.327c11.124 0 23.105 8.899 23.105 23.105 0 14.721-8.388 23.105-23.105 23.105z" />
<glyph unicode="&#xe90f;" glyph-name="copy-disabled" data-tags="copy-disabled" d="M614.525 809.292h-317.672c-74.968 0-135.9-60.931-135.9-135.9v-370.388c0-43.817 20.88-82.842 53.231-107.83v70.346c-4.45 11.297-7.016 23.962-7.016 37.484v370.388c0 49.292 40.224 89.516 89.516 89.516h318.014c15.232 0 29.611-3.766 42.107-10.613h68.294c-24.646 34.402-65.039 56.997-110.57 56.997zM726.978 686.23h-318.014c-74.968 0-135.9-60.931-135.9-135.9v-370.388c0-74.968 60.931-135.9 135.9-135.9v0h318.183c74.968 0 135.9 60.931 135.9 135.9v370.388c-0.342 74.968-61.273 135.9-136.073 135.9zM816.494 183.878c0-49.292-40.224-89.516-89.516-89.516h-318.183c-49.292 0-89.516 40.224-89.516 89.516v366.449c0 49.292 40.224 89.516 89.516 89.516h318.014c49.292 0 89.516-40.224 89.516-89.516v-349.334h0.173v-17.115zM709.521 468.857l-27.728 27.555-109.544-109.544-109.544 109.544-27.555-27.555 109.544-109.544-109.544-109.544 27.555-27.555 109.544 109.544 109.544-109.544 27.555 27.555-109.544 109.544 109.713 109.544z" />
<glyph unicode="&#xe910;" glyph-name="paste-enabled" data-tags="paste-enabled" d="M410.237 57.275c-75.402 0-136.793 61.394-136.793 136.793v373.025c0 75.402 61.394 136.793 136.793 136.793h64.85l4.152 11.413c15.219 41.678 47.732 65.715 89.237 65.715 42.716 0 78.512-25.075 93.212-65.715l4.152-11.413h64.85c37.007 0 73.499-15.911 99.786-43.581 25.594-26.805 38.737-61.048 37.007-96.326v-369.738c0-75.402-61.394-136.793-136.793-136.793l-320.453-0.173zM360.947 638.689c-24.729-15.046-40.64-44.619-40.64-75.575v-373.025c0-49.804 40.467-90.271 90.271-90.271h324.432c43.754 0 80.415 31.476 88.545 72.981h1.73l0.173 17.295v373.025c0 28.708-14.181 58.627-35.277 74.71l-27.67 20.923v-17.468c0-47.213-36.834-84.048-84.048-84.048h-207.351c-47.213 0-84.048 36.834-84.048 84.048v13.489l-26.113-16.084zM572.451 733.631c-27.843 0-48.766-20.923-48.766-48.766 0-26.459 22.307-48.766 48.766-48.766s48.766 22.307 48.766 48.766c0.173 26.286-22.134 48.766-48.766 48.766zM422.169 764.069c7.78 8.818 16.603 16.776 24.383 25.594 1.903 2.076 3.806 4.325 5.709 6.401h-158.585c-75.748 0-137.312-61.567-137.312-137.312v-374.236c0-44.965 21.788-84.913 55.167-109.988v68.829c-5.536 12.278-8.472 26.113-8.472 41.159v374.236c0 49.804 40.64 90.444 90.444 90.444h116.561c3.633 5.19 7.78 10.202 12.105 14.873z" />
<glyph unicode="&#xe911;" glyph-name="paste-disabled" data-tags="paste-disabled" d="M410.237 57.275c-75.402 0-136.793 61.394-136.793 136.793v373.025c0 75.402 61.394 136.793 136.793 136.793h64.85l4.152 11.413c15.219 41.678 47.732 65.715 89.237 65.715 42.716 0 78.512-25.075 93.212-65.715l4.152-11.413h64.85c37.007 0 73.499-15.911 99.786-43.581 25.594-26.805 38.737-61.048 37.007-96.326v-369.738c0-75.402-61.394-136.793-136.793-136.793l-320.453-0.173zM360.947 638.689c-24.729-15.046-40.64-44.619-40.64-75.575v-373.025c0-49.804 40.467-90.271 90.271-90.271h324.432c43.754 0 80.415 31.476 88.545 72.981h1.73l0.173 17.295v373.025c0 28.708-14.181 58.627-35.277 74.71l-27.67 20.923v-17.468c0-47.213-36.834-84.048-84.048-84.048h-207.351c-47.213 0-84.048 36.834-84.048 84.048v13.489l-26.113-16.084zM572.451 733.631c-27.843 0-48.766-20.923-48.766-48.766 0-26.459 22.307-48.766 48.766-48.766s48.766 22.307 48.766 48.766c0.173 26.286-22.134 48.766-48.766 48.766zM422.169 764.069c7.78 8.818 16.603 16.776 24.383 25.594 1.903 2.076 3.806 4.325 5.709 6.401h-158.585c-75.748 0-137.312-61.567-137.312-137.312v-374.236c0-44.965 21.788-84.913 55.167-109.988v68.829c-5.536 12.278-8.472 26.113-8.472 41.159v374.236c0 49.804 40.64 90.444 90.444 90.444h116.561c3.633 5.19 7.78 10.202 12.105 14.873zM718.585 463.848l-28.016 27.843-110.68-110.68-110.68 110.68-27.843-27.843 110.68-110.68-110.68-110.68 27.843-27.843 110.68 110.68 110.68-110.68 27.843 27.843-110.68 110.68 110.853 110.68z" />
<glyph unicode="&#xe912;" glyph-name="button-disabled" data-tags="button-disabled" d="M853.333 699.246l-68.754 68.754-272.579-272.579-272.579 272.579-68.754-68.754 272.579-272.579-272.579-272.579 68.754-68.754 272.579 272.579 272.579-272.579 68.754 68.754-272.579 272.579z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,100 @@
/**
* @class
* @augments H5P.EventDispatcher
* @param {Object} displayOptions
* @param {boolean} displayOptions.export Triggers the display of the 'Download' button
* @param {boolean} displayOptions.copyright Triggers the display of the 'Copyright' button
* @param {boolean} displayOptions.embed Triggers the display of the 'Embed' button
* @param {boolean} displayOptions.icon Triggers the display of the 'H5P icon' link
*/
H5P.ActionBar = (function ($, EventDispatcher) {
"use strict";
function ActionBar(displayOptions) {
EventDispatcher.call(this);
/** @alias H5P.ActionBar# */
var self = this;
var hasActions = false;
// Create action bar
var $actions = H5P.jQuery('<ul class="h5p-actions"></ul>');
/**
* Helper for creating action bar buttons.
*
* @private
* @param {string} type
* @param {string} customClass Instead of type class
*/
var addActionButton = function (type, customClass) {
/**
* Handles selection of action
*/
var handler = function () {
self.trigger(type);
};
H5P.jQuery('<li/>', {
'class': 'h5p-button h5p-noselect h5p-' + (customClass ? customClass : type),
role: 'button',
tabindex: 0,
title: H5P.t(type + 'Description'),
html: H5P.t(type),
on: {
click: handler,
keypress: function (e) {
if (e.which === 32) {
handler();
e.preventDefault(); // (since return false will block other inputs)
}
}
},
appendTo: $actions
});
hasActions = true;
};
// Register action bar buttons
if (displayOptions.export || displayOptions.copy) {
// Add export button
addActionButton('reuse', 'export');
}
if (displayOptions.copyright) {
addActionButton('copyrights');
}
if (displayOptions.embed) {
addActionButton('embed');
}
if (displayOptions.icon) {
// Add about H5P button icon
H5P.jQuery('<li><a class="h5p-link" href="http://h5p.org" target="_blank" title="' + H5P.t('h5pDescription') + '"></a></li>').appendTo($actions);
hasActions = true;
}
/**
* Returns a reference to the dom element
*
* @return {H5P.jQuery}
*/
self.getDOMElement = function () {
return $actions;
};
/**
* Does the actionbar contain actions?
*
* @return {Boolean}
*/
self.hasActions = function () {
return hasActions;
};
}
ActionBar.prototype = Object.create(EventDispatcher.prototype);
ActionBar.prototype.constructor = ActionBar;
return ActionBar;
})(H5P.jQuery, H5P.EventDispatcher);

View File

@ -0,0 +1,410 @@
/*global H5P*/
H5P.ConfirmationDialog = (function (EventDispatcher) {
"use strict";
/**
* Create a confirmation dialog
*
* @param [options] Options for confirmation dialog
* @param [options.instance] Instance that uses confirmation dialog
* @param [options.headerText] Header text
* @param [options.dialogText] Dialog text
* @param [options.cancelText] Cancel dialog button text
* @param [options.confirmText] Confirm dialog button text
* @param [options.hideCancel] Hide cancel button
* @param [options.hideExit] Hide exit button
* @param [options.skipRestoreFocus] Skip restoring focus when hiding the dialog
* @param [options.classes] Extra classes for popup
* @constructor
*/
function ConfirmationDialog(options) {
EventDispatcher.call(this);
var self = this;
// Make sure confirmation dialogs have unique id
H5P.ConfirmationDialog.uniqueId += 1;
var uniqueId = H5P.ConfirmationDialog.uniqueId;
// Default options
options = options || {};
options.headerText = options.headerText || H5P.t('confirmDialogHeader');
options.dialogText = options.dialogText || H5P.t('confirmDialogBody');
options.cancelText = options.cancelText || H5P.t('cancelLabel');
options.confirmText = options.confirmText || H5P.t('confirmLabel');
/**
* Handle confirming event
* @param {Event} e
*/
function dialogConfirmed(e) {
self.hide();
self.trigger('confirmed');
e.preventDefault();
}
/**
* Handle dialog canceled
* @param {Event} e
*/
function dialogCanceled(e) {
self.hide();
self.trigger('canceled');
e.preventDefault();
}
/**
* Flow focus to element
* @param {HTMLElement} element Next element to be focused
* @param {Event} e Original tab event
*/
function flowTo(element, e) {
element.focus();
e.preventDefault();
}
// Offset of exit button
var exitButtonOffset = 2 * 16;
var shadowOffset = 8;
// Determine if we are too large for our container and must resize
var resizeIFrame = false;
// Create background
var popupBackground = document.createElement('div');
popupBackground.classList
.add('h5p-confirmation-dialog-background', 'hidden', 'hiding');
// Create outer popup
var popup = document.createElement('div');
popup.classList.add('h5p-confirmation-dialog-popup', 'hidden');
if (options.classes) {
options.classes.forEach(function (popupClass) {
popup.classList.add(popupClass);
});
}
popup.setAttribute('role', 'dialog');
popup.setAttribute('aria-labelledby', 'h5p-confirmation-dialog-dialog-text-' + uniqueId);
popupBackground.appendChild(popup);
popup.addEventListener('keydown', function (e) {
if (e.which === 27) {// Esc key
// Exit dialog
dialogCanceled(e);
}
});
// Popup header
var header = document.createElement('div');
header.classList.add('h5p-confirmation-dialog-header');
popup.appendChild(header);
// Header text
var headerText = document.createElement('div');
headerText.classList.add('h5p-confirmation-dialog-header-text');
headerText.innerHTML = options.headerText;
header.appendChild(headerText);
// Popup body
var body = document.createElement('div');
body.classList.add('h5p-confirmation-dialog-body');
popup.appendChild(body);
// Popup text
var text = document.createElement('div');
text.classList.add('h5p-confirmation-dialog-text');
text.innerHTML = options.dialogText;
text.id = 'h5p-confirmation-dialog-dialog-text-' + uniqueId;
body.appendChild(text);
// Popup buttons
var buttons = document.createElement('div');
buttons.classList.add('h5p-confirmation-dialog-buttons');
body.appendChild(buttons);
// Cancel button
var cancelButton = document.createElement('button');
cancelButton.classList.add('h5p-core-cancel-button');
cancelButton.textContent = options.cancelText;
// Confirm button
var confirmButton = document.createElement('button');
confirmButton.classList.add('h5p-core-button');
confirmButton.classList.add('h5p-confirmation-dialog-confirm-button');
confirmButton.textContent = options.confirmText;
// Exit button
var exitButton = document.createElement('button');
exitButton.classList.add('h5p-confirmation-dialog-exit');
exitButton.setAttribute('aria-hidden', 'true');
exitButton.tabIndex = -1;
exitButton.title = options.cancelText;
// Cancel handler
cancelButton.addEventListener('click', dialogCanceled);
cancelButton.addEventListener('keydown', function (e) {
if (e.which === 32) { // Space
dialogCanceled(e);
}
else if (e.which === 9 && e.shiftKey) { // Shift-tab
flowTo(confirmButton, e);
}
});
if (!options.hideCancel) {
buttons.appendChild(cancelButton);
}
else {
// Center buttons
buttons.classList.add('center');
}
// Confirm handler
confirmButton.addEventListener('click', dialogConfirmed);
confirmButton.addEventListener('keydown', function (e) {
if (e.which === 32) { // Space
dialogConfirmed(e);
}
else if (e.which === 9 && !e.shiftKey) { // Tab
const nextButton = !options.hideCancel ? cancelButton : confirmButton;
flowTo(nextButton, e);
}
});
buttons.appendChild(confirmButton);
// Exit handler
exitButton.addEventListener('click', dialogCanceled);
exitButton.addEventListener('keydown', function (e) {
if (e.which === 32) { // Space
dialogCanceled(e);
}
});
if (!options.hideExit) {
popup.appendChild(exitButton);
}
// Wrapper element
var wrapperElement;
// Focus capturing
var focusPredator;
// Maintains hidden state of elements
var wrapperSiblingsHidden = [];
var popupSiblingsHidden = [];
// Element with focus before dialog
var previouslyFocused;
/**
* Set parent of confirmation dialog
* @param {HTMLElement} wrapper
* @returns {H5P.ConfirmationDialog}
*/
this.appendTo = function (wrapper) {
wrapperElement = wrapper;
return this;
};
/**
* Capture the focus element, send it to confirmation button
* @param {Event} e Original focus event
*/
var captureFocus = function (e) {
if (!popupBackground.contains(e.target)) {
e.preventDefault();
confirmButton.focus();
}
};
/**
* Hide siblings of element from assistive technology
*
* @param {HTMLElement} element
* @returns {Array} The previous hidden state of all siblings
*/
var hideSiblings = function (element) {
var hiddenSiblings = [];
var siblings = element.parentNode.children;
var i;
for (i = 0; i < siblings.length; i += 1) {
// Preserve hidden state
hiddenSiblings[i] = siblings[i].getAttribute('aria-hidden') ?
true : false;
if (siblings[i] !== element) {
siblings[i].setAttribute('aria-hidden', true);
}
}
return hiddenSiblings;
};
/**
* Restores assistive technology state of element's siblings
*
* @param {HTMLElement} element
* @param {Array} hiddenSiblings Hidden state of all siblings
*/
var restoreSiblings = function (element, hiddenSiblings) {
var siblings = element.parentNode.children;
var i;
for (i = 0; i < siblings.length; i += 1) {
if (siblings[i] !== element && !hiddenSiblings[i]) {
siblings[i].removeAttribute('aria-hidden');
}
}
};
/**
* Start capturing focus of parent and send it to dialog
*/
var startCapturingFocus = function () {
focusPredator = wrapperElement.parentNode || wrapperElement;
focusPredator.addEventListener('focus', captureFocus, true);
};
/**
* Clean up event listener for capturing focus
*/
var stopCapturingFocus = function () {
focusPredator.removeAttribute('aria-hidden');
focusPredator.removeEventListener('focus', captureFocus, true);
};
/**
* Hide siblings in underlay from assistive technologies
*/
var disableUnderlay = function () {
wrapperSiblingsHidden = hideSiblings(wrapperElement);
popupSiblingsHidden = hideSiblings(popupBackground);
};
/**
* Restore state of underlay for assistive technologies
*/
var restoreUnderlay = function () {
restoreSiblings(wrapperElement, wrapperSiblingsHidden);
restoreSiblings(popupBackground, popupSiblingsHidden);
};
/**
* Fit popup to container. Makes sure it doesn't overflow.
* @params {number} [offsetTop] Offset of popup
*/
var fitToContainer = function (offsetTop) {
var popupOffsetTop = parseInt(popup.style.top, 10);
if (offsetTop !== undefined) {
popupOffsetTop = offsetTop;
}
if (!popupOffsetTop) {
popupOffsetTop = 0;
}
// Overflows height
if (popupOffsetTop + popup.offsetHeight > wrapperElement.offsetHeight) {
popupOffsetTop = wrapperElement.offsetHeight - popup.offsetHeight - shadowOffset;
}
if (popupOffsetTop - exitButtonOffset <= 0) {
popupOffsetTop = exitButtonOffset + shadowOffset;
// We are too big and must resize
resizeIFrame = true;
}
popup.style.top = popupOffsetTop + 'px';
};
/**
* Show confirmation dialog
* @params {number} offsetTop Offset top
* @returns {H5P.ConfirmationDialog}
*/
this.show = function (offsetTop) {
// Capture focused item
previouslyFocused = document.activeElement;
wrapperElement.appendChild(popupBackground);
startCapturingFocus();
disableUnderlay();
popupBackground.classList.remove('hidden');
fitToContainer(offsetTop);
setTimeout(function () {
popup.classList.remove('hidden');
popupBackground.classList.remove('hiding');
setTimeout(function () {
// Focus confirm button
confirmButton.focus();
// Resize iFrame if necessary
if (resizeIFrame && options.instance) {
var minHeight = parseInt(popup.offsetHeight, 10) +
exitButtonOffset + (2 * shadowOffset);
self.setViewPortMinimumHeight(minHeight);
options.instance.trigger('resize');
resizeIFrame = false;
}
}, 100);
}, 0);
return this;
};
/**
* Hide confirmation dialog
* @returns {H5P.ConfirmationDialog}
*/
this.hide = function () {
popupBackground.classList.add('hiding');
popup.classList.add('hidden');
// Restore focus
stopCapturingFocus();
if (!options.skipRestoreFocus) {
previouslyFocused.focus();
}
restoreUnderlay();
setTimeout(function () {
popupBackground.classList.add('hidden');
wrapperElement.removeChild(popupBackground);
self.setViewPortMinimumHeight(null);
}, 100);
return this;
};
/**
* Retrieve element
*
* @return {HTMLElement}
*/
this.getElement = function () {
return popup;
};
/**
* Get previously focused element
* @return {HTMLElement}
*/
this.getPreviouslyFocused = function () {
return previouslyFocused;
};
/**
* Sets the minimum height of the view port
*
* @param {number|null} minHeight
*/
this.setViewPortMinimumHeight = function (minHeight) {
var container = document.querySelector('.h5p-container') || document.body;
container.style.minHeight = (typeof minHeight === 'number') ? (minHeight + 'px') : minHeight;
};
}
ConfirmationDialog.prototype = Object.create(EventDispatcher.prototype);
ConfirmationDialog.prototype.constructor = ConfirmationDialog;
return ConfirmationDialog;
}(H5P.EventDispatcher));
H5P.ConfirmationDialog.uniqueId = -1;

View File

@ -0,0 +1,41 @@
/**
* H5P.ContentType is a base class for all content types. Used by newRunnable()
*
* Functions here may be overridable by the libraries. In special cases,
* it is also possible to override H5P.ContentType on a global level.
*
* NOTE that this doesn't actually 'extend' the event dispatcher but instead
* it creates a single instance which all content types shares as their base
* prototype. (in some cases this may be the root of strange event behavior)
*
* @class
* @augments H5P.EventDispatcher
*/
H5P.ContentType = function (isRootLibrary) {
function ContentType() {}
// Inherit from EventDispatcher.
ContentType.prototype = new H5P.EventDispatcher();
/**
* Is library standalone or not? Not beeing standalone, means it is
* included in another library
*
* @return {Boolean}
*/
ContentType.prototype.isRoot = function () {
return isRootLibrary;
};
/**
* Returns the file path of a file in the current library
* @param {string} filePath The path to the file relative to the library folder
* @return {string} The full path to the file
*/
ContentType.prototype.getLibraryFilePath = function (filePath) {
return H5P.getLibraryPath(this.libraryInfo.versionedNameNoSpaces) + '/' + filePath;
};
return ContentType;
};

View File

@ -0,0 +1,313 @@
/*jshint -W083 */
var H5PUpgrades = H5PUpgrades || {};
H5P.ContentUpgradeProcess = (function (Version) {
/**
* @class
* @namespace H5P
*/
function ContentUpgradeProcess(name, oldVersion, newVersion, params, id, loadLibrary, done) {
var self = this;
// Make params possible to work with
try {
params = JSON.parse(params);
if (!(params instanceof Object)) {
throw true;
}
}
catch (event) {
return done({
type: 'errorParamsBroken',
id: id
});
}
self.loadLibrary = loadLibrary;
self.upgrade(name, oldVersion, newVersion, params.params, params.metadata, function (err, upgradedParams, upgradedMetadata) {
if (err) {
err.id = id;
return done(err);
}
done(null, JSON.stringify({params: upgradedParams, metadata: upgradedMetadata}));
});
}
/**
* Run content upgrade.
*
* @public
* @param {string} name
* @param {Version} oldVersion
* @param {Version} newVersion
* @param {Object} params
* @param {Object} metadata
* @param {Function} done
*/
ContentUpgradeProcess.prototype.upgrade = function (name, oldVersion, newVersion, params, metadata, done) {
var self = this;
// Load library details and upgrade routines
self.loadLibrary(name, newVersion, function (err, library) {
if (err) {
return done(err);
}
if (library.semantics === null) {
return done({
type: 'libraryMissing',
library: library.name + ' ' + library.version.major + '.' + library.version.minor
});
}
// Run upgrade routines on params
self.processParams(library, oldVersion, newVersion, params, metadata, function (err, params, metadata) {
if (err) {
return done(err);
}
// Check if any of the sub-libraries need upgrading
asyncSerial(library.semantics, function (index, field, next) {
self.processField(field, params[field.name], function (err, upgradedParams) {
if (upgradedParams) {
params[field.name] = upgradedParams;
}
next(err);
});
}, function (err) {
done(err, params, metadata);
});
});
});
};
/**
* Run upgrade hooks on params.
*
* @public
* @param {Object} library
* @param {Version} oldVersion
* @param {Version} newVersion
* @param {Object} params
* @param {Function} next
*/
ContentUpgradeProcess.prototype.processParams = function (library, oldVersion, newVersion, params, metadata, next) {
if (H5PUpgrades[library.name] === undefined) {
if (library.upgradesScript) {
// Upgrades script should be loaded so the upgrades should be here.
return next({
type: 'scriptMissing',
library: library.name + ' ' + newVersion
});
}
// No upgrades script. Move on
return next(null, params, metadata);
}
// Run upgrade hooks. Start by going through major versions
asyncSerial(H5PUpgrades[library.name], function (major, minors, nextMajor) {
if (major < oldVersion.major || major > newVersion.major) {
// Older than the current version or newer than the selected
nextMajor();
}
else {
// Go through the minor versions for this major version
asyncSerial(minors, function (minor, upgrade, nextMinor) {
minor =+ minor;
if (minor <= oldVersion.minor || minor > newVersion.minor) {
// Older than or equal to the current version or newer than the selected
nextMinor();
}
else {
// We found an upgrade hook, run it
var unnecessaryWrapper = (upgrade.contentUpgrade !== undefined ? upgrade.contentUpgrade : upgrade);
try {
unnecessaryWrapper(params, function (err, upgradedParams, upgradedExtras) {
params = upgradedParams;
if (upgradedExtras && upgradedExtras.metadata) { // Optional
metadata = upgradedExtras.metadata;
}
nextMinor(err);
}, {metadata: metadata});
}
catch (err) {
if (console && console.error) {
console.error("Error", err.stack);
console.error("Error", err.name);
console.error("Error", err.message);
}
next(err);
}
}
}, nextMajor);
}
}, function (err) {
next(err, params, metadata);
});
};
/**
* Process parameter fields to find and upgrade sub-libraries.
*
* @public
* @param {Object} field
* @param {Object} params
* @param {Function} done
*/
ContentUpgradeProcess.prototype.processField = function (field, params, done) {
var self = this;
if (params === undefined) {
return done();
}
switch (field.type) {
case 'library':
if (params.library === undefined || params.params === undefined) {
return done();
}
// Look for available upgrades
var usedLib = params.library.split(' ', 2);
for (var i = 0; i < field.options.length; i++) {
var availableLib = (typeof field.options[i] === 'string') ? field.options[i].split(' ', 2) : field.options[i].name.split(' ', 2);
if (availableLib[0] === usedLib[0]) {
if (availableLib[1] === usedLib[1]) {
return done(); // Same version
}
// We have different versions
var usedVer = new Version(usedLib[1]);
var availableVer = new Version(availableLib[1]);
if (usedVer.major > availableVer.major || (usedVer.major === availableVer.major && usedVer.minor >= availableVer.minor)) {
return done({
type: 'errorTooHighVersion',
used: usedLib[0] + ' ' + usedVer,
supported: availableLib[0] + ' ' + availableVer
}); // Larger or same version that's available
}
// A newer version is available, upgrade params
return self.upgrade(availableLib[0], usedVer, availableVer, params.params, params.metadata, function (err, upgradedParams, upgradedMetadata) {
if (!err) {
params.library = availableLib[0] + ' ' + availableVer.major + '.' + availableVer.minor;
params.params = upgradedParams;
if (upgradedMetadata) {
params.metadata = upgradedMetadata;
}
}
done(err, params);
});
}
}
// Content type was not supporte by the higher version
done({
type: 'errorNotSupported',
used: usedLib[0] + ' ' + usedVer
});
break;
case 'group':
if (field.fields.length === 1 && field.isSubContent !== true) {
// Single field to process, wrapper will be skipped
self.processField(field.fields[0], params, function (err, upgradedParams) {
if (upgradedParams) {
params = upgradedParams;
}
done(err, params);
});
}
else {
// Go through all fields in the group
asyncSerial(field.fields, function (index, subField, next) {
var paramsToProcess = params ? params[subField.name] : null;
self.processField(subField, paramsToProcess, function (err, upgradedParams) {
if (upgradedParams) {
params[subField.name] = upgradedParams;
}
next(err);
});
}, function (err) {
done(err, params);
});
}
break;
case 'list':
// Go trough all params in the list
asyncSerial(params, function (index, subParams, next) {
self.processField(field.field, subParams, function (err, upgradedParams) {
if (upgradedParams) {
params[index] = upgradedParams;
}
next(err);
});
}, function (err) {
done(err, params);
});
break;
default:
done();
}
};
/**
* Helps process each property on the given object asynchronously in serial order.
*
* @private
* @param {Object} obj
* @param {Function} process
* @param {Function} finished
*/
var asyncSerial = function (obj, process, finished) {
var id, isArray = obj instanceof Array;
// Keep track of each property that belongs to this object.
if (!isArray) {
var ids = [];
for (id in obj) {
if (obj.hasOwnProperty(id)) {
ids.push(id);
}
}
}
var i = -1; // Keeps track of the current property
/**
* Private. Process the next property
*/
var next = function () {
id = isArray ? i : ids[i];
process(id, obj[id], check);
};
/**
* Private. Check if we're done or have an error.
*
* @param {String} err
*/
var check = function (err) {
// We need to use a real async function in order for the stack to clear.
setTimeout(function () {
i++;
if (i === (isArray ? obj.length : ids.length) || (err !== undefined && err !== null)) {
finished(err);
}
else {
next();
}
}, 0);
};
check(); // Start
};
return ContentUpgradeProcess;
})(H5P.Version);

View File

@ -0,0 +1,63 @@
/* global importScripts */
var H5P = H5P || {};
importScripts('h5p-version.js', 'h5p-content-upgrade-process.js');
var libraryLoadedCallback;
/**
* Register message handlers
*/
var messageHandlers = {
newJob: function (job) {
// Start new job
new H5P.ContentUpgradeProcess(job.name, new H5P.Version(job.oldVersion), new H5P.Version(job.newVersion), job.params, job.id, function loadLibrary(name, version, next) {
// TODO: Cache?
postMessage({
action: 'loadLibrary',
name: name,
version: version.toString()
});
libraryLoadedCallback = next;
}, function done(err, result) {
if (err) {
// Return error
postMessage({
action: 'error',
id: job.id,
err: err.message ? err.message : err
});
return;
}
// Return upgraded content
postMessage({
action: 'done',
id: job.id,
params: result
});
});
},
libraryLoaded: function (data) {
var library = data.library;
if (library.upgradesScript) {
try {
importScripts(library.upgradesScript);
}
catch (err) {
libraryLoadedCallback(err);
return;
}
}
libraryLoadedCallback(null, data.library);
}
};
/**
* Handle messages from our master
*/
onmessage = function (event) {
if (event.data.action !== undefined && messageHandlers[event.data.action]) {
messageHandlers[event.data.action].call(this, event.data);
}
};

View File

@ -0,0 +1,445 @@
/* global H5PAdminIntegration H5PUtils */
(function ($, Version) {
var info, $log, $container, librariesCache = {}, scriptsCache = {};
// Initialize
$(document).ready(function () {
// Get library info
info = H5PAdminIntegration.libraryInfo;
// Get and reset container
const $wrapper = $('#h5p-admin-container').html('');
$log = $('<ul class="content-upgrade-log"></ul>').appendTo($wrapper);
$container = $('<div><p>' + info.message + '</p></div>').appendTo($wrapper);
// Make it possible to select version
var $version = $(getVersionSelect(info.versions)).appendTo($container);
// Add "go" button
$('<button/>', {
class: 'h5p-admin-upgrade-button',
text: info.buttonLabel,
click: function () {
// Start new content upgrade
new ContentUpgrade($version.val());
}
}).appendTo($container);
});
/**
* Generate html for version select.
*
* @param {Object} versions
* @returns {String}
*/
var getVersionSelect = function (versions) {
var html = '';
for (var id in versions) {
html += '<option value="' + id + '">' + versions[id] + '</option>';
}
if (html !== '') {
html = '<select>' + html + '</select>';
return html;
}
};
/**
* Displays a throbber in the status field.
*
* @param {String} msg
* @returns {_L1.Throbber}
*/
function Throbber(msg) {
var $throbber = H5PUtils.throbber(msg);
$container.html('').append($throbber);
/**
* Makes it possible to set the progress.
*
* @param {String} progress
*/
this.setProgress = function (progress) {
$throbber.text(msg + ' ' + progress);
};
}
/**
* Start a new content upgrade.
*
* @param {Number} libraryId
* @returns {_L1.ContentUpgrade}
*/
function ContentUpgrade(libraryId) {
var self = this;
// Get selected version
self.version = new Version(info.versions[libraryId]);
self.version.libraryId = libraryId;
// Create throbber with loading text and progress
self.throbber = new Throbber(info.inProgress.replace('%ver', self.version));
self.started = new Date().getTime();
self.io = 0;
// Track number of working
self.working = 0;
var start = function () {
// Get the next batch
self.nextBatch({
libraryId: libraryId,
token: info.token
});
};
if (window.Worker !== undefined) {
// Prepare our workers
self.initWorkers();
start();
}
else {
// No workers, do the job ourselves
self.loadScript(info.scriptBaseUrl + '/h5p-content-upgrade-process.js' + info.buster, start);
}
}
/**
* Initialize workers
*/
ContentUpgrade.prototype.initWorkers = function () {
var self = this;
// Determine number of workers (defaults to 4)
var numWorkers = (window.navigator !== undefined && window.navigator.hardwareConcurrency ? window.navigator.hardwareConcurrency : 4);
self.workers = new Array(numWorkers);
// Register message handlers
var messageHandlers = {
done: function (result) {
self.workDone(result.id, result.params, this);
},
error: function (error) {
self.printError(error.err);
self.workDone(error.id, null, this);
},
loadLibrary: function (details) {
var worker = this;
self.loadLibrary(details.name, new Version(details.version), function (err, library) {
if (err) {
// Reset worker?
return;
}
worker.postMessage({
action: 'libraryLoaded',
library: library
});
});
}
};
for (var i = 0; i < numWorkers; i++) {
self.workers[i] = new Worker(info.scriptBaseUrl + '/h5p-content-upgrade-worker.js' + info.buster);
self.workers[i].onmessage = function (event) {
if (event.data.action !== undefined && messageHandlers[event.data.action]) {
messageHandlers[event.data.action].call(this, event.data);
}
};
}
};
/**
* Get the next batch and start processing it.
*
* @param {Object} outData
*/
ContentUpgrade.prototype.nextBatch = function (outData) {
var self = this;
// Track time spent on IO
var start = new Date().getTime();
$.post(info.infoUrl, outData, function (inData) {
self.io += new Date().getTime() - start;
if (!(inData instanceof Object)) {
// Print errors from backend
return self.setStatus(inData);
}
if (inData.left === 0) {
var total = new Date().getTime() - self.started;
if (window.console && console.log) {
console.log('The upgrade process took ' + (total / 1000) + ' seconds. (' + (Math.round((self.io / (total / 100)) * 100) / 100) + ' % IO)' );
}
// Terminate workers
self.terminate();
// Nothing left to process
return self.setStatus(info.done);
}
self.left = inData.left;
self.token = inData.token;
// Start processing
self.processBatch(inData.params, inData.skipped);
});
};
/**
* Set current status message.
*
* @param {String} msg
*/
ContentUpgrade.prototype.setStatus = function (msg) {
$container.html(msg);
};
/**
* Process the given parameters.
*
* @param {Object} parameters
*/
ContentUpgrade.prototype.processBatch = function (parameters, skipped) {
var self = this;
// Track upgraded params
self.upgraded = {};
self.skipped = skipped;
// Track current batch
self.parameters = parameters;
// Create id mapping
self.ids = [];
for (var id in parameters) {
if (parameters.hasOwnProperty(id)) {
self.ids.push(id);
}
}
// Keep track of current content
self.current = -1;
if (self.workers !== undefined) {
// Assign each worker content to upgrade
for (var i = 0; i < self.workers.length; i++) {
self.assignWork(self.workers[i]);
}
}
else {
self.assignWork();
}
};
/**
*
*/
ContentUpgrade.prototype.assignWork = function (worker) {
var self = this;
var id = self.ids[self.current + 1];
if (id === undefined) {
return false; // Out of work
}
self.current++;
self.working++;
if (worker) {
worker.postMessage({
action: 'newJob',
id: id,
name: info.library.name,
oldVersion: info.library.version,
newVersion: self.version.toString(),
params: self.parameters[id]
});
}
else {
new H5P.ContentUpgradeProcess(info.library.name, new Version(info.library.version), self.version, self.parameters[id], id, function loadLibrary(name, version, next) {
self.loadLibrary(name, version, function (err, library) {
if (library.upgradesScript) {
self.loadScript(library.upgradesScript, function (err) {
if (err) {
err = info.errorScript.replace('%lib', name + ' ' + version);
}
next(err, library);
});
}
else {
next(null, library);
}
});
}, function done(err, result) {
if (err) {
self.printError(err);
result = null;
}
self.workDone(id, result);
});
}
};
/**
*
*/
ContentUpgrade.prototype.workDone = function (id, result, worker) {
var self = this;
self.working--;
if (result === null) {
self.skipped.push(id);
}
else {
self.upgraded[id] = result;
}
// Update progress message
self.throbber.setProgress(Math.round((info.total - self.left + self.current) / (info.total / 100)) + ' %');
// Assign next job
if (self.assignWork(worker) === false && self.working === 0) {
// All workers have finsihed.
self.nextBatch({
libraryId: self.version.libraryId,
token: self.token,
skipped: JSON.stringify(self.skipped),
params: JSON.stringify(self.upgraded)
});
}
};
/**
*
*/
ContentUpgrade.prototype.terminate = function () {
var self = this;
if (self.workers) {
// Stop all workers
for (var i = 0; i < self.workers.length; i++) {
self.workers[i].terminate();
}
}
};
var librariesLoadedCallbacks = {};
/**
* Load library data needed for content upgrade.
*
* @param {String} name
* @param {Version} version
* @param {Function} next
*/
ContentUpgrade.prototype.loadLibrary = function (name, version, next) {
var self = this;
var key = name + '/' + version.major + '/' + version.minor;
if (librariesCache[key] === true) {
// Library is being loaded, que callback
if (librariesLoadedCallbacks[key] === undefined) {
librariesLoadedCallbacks[key] = [next];
return;
}
librariesLoadedCallbacks[key].push(next);
return;
}
else if (librariesCache[key] !== undefined) {
// Library has been loaded before. Return cache.
next(null, librariesCache[key]);
return;
}
// Track time spent loading
var start = new Date().getTime();
librariesCache[key] = true;
$.ajax({
dataType: 'json',
cache: true,
url: info.libraryBaseUrl + '/' + key
}).fail(function () {
self.io += new Date().getTime() - start;
next(info.errorData.replace('%lib', name + ' ' + version));
}).done(function (library) {
self.io += new Date().getTime() - start;
librariesCache[key] = library;
next(null, library);
if (librariesLoadedCallbacks[key] !== undefined) {
for (var i = 0; i < librariesLoadedCallbacks[key].length; i++) {
librariesLoadedCallbacks[key][i](null, library);
}
}
delete librariesLoadedCallbacks[key];
});
};
/**
* Load script with upgrade hooks.
*
* @param {String} url
* @param {Function} next
*/
ContentUpgrade.prototype.loadScript = function (url, next) {
var self = this;
if (scriptsCache[url] !== undefined) {
next();
return;
}
// Track time spent loading
var start = new Date().getTime();
$.ajax({
dataType: 'script',
cache: true,
url: url
}).fail(function () {
self.io += new Date().getTime() - start;
next(true);
}).done(function () {
scriptsCache[url] = true;
self.io += new Date().getTime() - start;
next();
});
};
/**
*
*/
ContentUpgrade.prototype.printError = function (error) {
var self = this;
switch (error.type) {
case 'errorParamsBroken':
error = info.errorContent.replace('%id', error.id) + ' ' + info.errorParamsBroken;
break;
case 'libraryMissing':
error = info.errorLibrary.replace('%lib', error.library);
break;
case 'scriptMissing':
error = info.errorScript.replace('%lib', error.library);
break;
case 'errorTooHighVersion':
error = info.errorContent.replace('%id', error.id) + ' ' + info.errorTooHighVersion.replace('%used', error.used).replace('%supported', error.supported);
break;
case 'errorNotSupported':
error = info.errorContent.replace('%id', error.id) + ' ' + info.errorNotSupported.replace('%used', error.used);
break;
}
$('<li>' + info.error + '<br/>' + error + '</li>').appendTo($log);
};
})(H5P.jQuery, H5P.Version);

View File

@ -0,0 +1,442 @@
/* global H5PUtils */
var H5PDataView = (function ($) {
/**
* Initialize a new H5P data view.
*
* @class
* @param {Object} container
* Element to clear out and append to.
* @param {String} source
* URL to get data from. Data format: {num: 123, rows:[[1,2,3],[2,4,6]]}
* @param {Array} headers
* List with column headers. Can be strings or objects with options like
* "text" and "sortable". E.g.
* [{text: 'Col 1', sortable: true}, 'Col 2', 'Col 3']
* @param {Object} l10n
* Localization / translations. e.g.
* {
* loading: 'Loading data.',
* ajaxFailed: 'Failed to load data.',
* noData: "There's no data available that matches your criteria.",
* currentPage: 'Page $current of $total',
* nextPage: 'Next page',
* previousPage: 'Previous page',
* search: 'Search'
* }
* @param {Object} classes
* Custom html classes to use on elements.
* e.g. {tableClass: 'fixed'}.
* @param {Array} filters
* Make it possible to filter/search in the given column.
* e.g. [null, true, null, null] will make it possible to do a text
* search in column 2.
* @param {Function} loaded
* Callback for when data has been loaded.
* @param {Object} order
*/
function H5PDataView(container, source, headers, l10n, classes, filters, loaded, order) {
var self = this;
self.$container = $(container).addClass('h5p-data-view').html('');
self.source = source;
self.headers = headers;
self.l10n = l10n;
self.classes = (classes === undefined ? {} : classes);
self.filters = (filters === undefined ? [] : filters);
self.loaded = loaded;
self.order = order;
self.limit = 20;
self.offset = 0;
self.filterOn = [];
self.facets = {};
// Index of column with author name; could be made more general by passing database column names and checking for position
self.columnIdAuthor = 2;
// Future option: Create more general solution for filter presets
if (H5PIntegration.user && parseInt(H5PIntegration.user.canToggleViewOthersH5PContents) === 1) {
self.updateTable([]);
self.filterByFacet(self.columnIdAuthor, H5PIntegration.user.id, H5PIntegration.user.name || '');
}
else {
self.loadData();
}
}
/**
* Load data from source URL.
*/
H5PDataView.prototype.loadData = function () {
var self = this;
// Throbb
self.setMessage(H5PUtils.throbber(self.l10n.loading));
// Create URL
var url = self.source;
url += (url.indexOf('?') === -1 ? '?' : '&') + 'offset=' + self.offset + '&limit=' + self.limit;
// Add sorting
if (self.order !== undefined) {
url += '&sortBy=' + self.order.by + '&sortDir=' + self.order.dir;
}
// Add filters
var filtering;
for (var i = 0; i < self.filterOn.length; i++) {
if (self.filterOn[i] === undefined) {
continue;
}
filtering = true;
url += '&filters[' + i + ']=' + encodeURIComponent(self.filterOn[i]);
}
// Add facets
for (var col in self.facets) {
if (!self.facets.hasOwnProperty(col)) {
continue;
}
url += '&facets[' + col + ']=' + self.facets[col].id;
}
// Fire ajax request
$.ajax({
dataType: 'json',
cache: true,
url: url
}).fail(function () {
// Error handling
self.setMessage($('<p/>', {text: self.l10n.ajaxFailed}));
}).done(function (data) {
if (!data.rows.length) {
self.setMessage($('<p/>', {text: filtering ? self.l10n.noData : self.l10n.empty}));
}
else {
// Update table data
self.updateTable(data.rows);
}
// Update pagination widget
self.updatePagination(data.num);
if (self.loaded !== undefined) {
self.loaded();
}
});
};
/**
* Display the given message to the user.
*
* @param {jQuery} $message wrapper with message
*/
H5PDataView.prototype.setMessage = function ($message) {
var self = this;
if (self.table === undefined) {
self.$container.html('').append($message);
}
else {
self.table.setBody($message);
}
};
/**
* Update table data.
*
* @param {Array} rows
*/
H5PDataView.prototype.updateTable = function (rows) {
var self = this;
if (self.table === undefined) {
// Clear out container
self.$container.html('');
// Add filters
self.addFilters();
// Add toggler for others' content
if (H5PIntegration.user && parseInt(H5PIntegration.user.canToggleViewOthersH5PContents) > 0) {
// canToggleViewOthersH5PContents = 1 is setting for only showing current user's contents
self.addOthersContentToggler(parseInt(H5PIntegration.user.canToggleViewOthersH5PContents) === 1);
}
// Add facets
self.$facets = $('<div/>', {
'class': 'h5p-facet-wrapper',
appendTo: self.$container
});
// Create new table
self.table = new H5PUtils.Table(self.classes, self.headers);
self.table.setHeaders(self.headers, function (order) {
// Sorting column or direction has changed.
self.order = order;
self.loadData();
}, self.order);
self.table.appendTo(self.$container);
}
// Process cell data before updating table
for (var i = 0; i < self.headers.length; i++) {
if (self.headers[i].facet === true) {
// Process rows for col, expect object or array
for (var j = 0; j < rows.length; j++) {
rows[j][i] = self.createFacets(rows[j][i], i);
}
}
}
// Add/update rows
var $tbody = self.table.setRows(rows);
// Add event handlers for facets
$('.h5p-facet', $tbody).click(function () {
var $facet = $(this);
self.filterByFacet($facet.data('col'), $facet.data('id'), $facet.text());
}).keypress(function (event) {
if (event.which === 32) {
var $facet = $(this);
self.filterByFacet($facet.data('col'), $facet.data('id'), $facet.text());
}
});
};
/**
* Create button for adding facet to filter.
*
* @param (object|Array) input
* @param number col ID of column
*/
H5PDataView.prototype.createFacets = function (input, col) {
var facets = '';
if (input instanceof Array) {
// Facet can be filtered on multiple values at the same time
for (var i = 0; i < input.length; i++) {
if (facets !== '') {
facets += ', ';
}
facets += '<span class="h5p-facet" role="button" tabindex="0" data-id="' + input[i].id + '" data-col="' + col + '">' + input[i].title + '</span>';
}
}
else {
// Single value facet filtering
facets += '<span class="h5p-facet" role="button" tabindex="0" data-id="' + input.id + '" data-col="' + col + '">' + input.title + '</span>';
}
return facets === '' ? '—' : facets;
};
/**
* Adds a filter based on the given facet.
*
* @param number col ID of column we're filtering
* @param number id ID to filter on
* @param string text Human readable label for the filter
*/
H5PDataView.prototype.filterByFacet = function (col, id, text) {
var self = this;
if (self.facets[col] !== undefined) {
if (self.facets[col].id === id) {
return; // Don't use the same filter again
}
// Remove current filter for this col
self.facets[col].$tag.remove();
}
// Add to UI
self.facets[col] = {
id: id,
'$tag': $('<span/>', {
'class': 'h5p-facet-tag',
text: text,
appendTo: self.$facets,
})
};
/**
* Callback for removing filter.
*
* @private
*/
var remove = function () {
// Uncheck toggler for others' H5P contents
if ( self.$othersContentToggler && self.facets.hasOwnProperty( self.columnIdAuthor ) ) {
self.$othersContentToggler.prop('checked', false );
}
self.facets[col].$tag.remove();
delete self.facets[col];
self.loadData();
};
// Remove button
$('<span/>', {
role: 'button',
tabindex: 0,
appendTo: self.facets[col].$tag,
text: self.l10n.remove,
title: self.l10n.remove,
on: {
click: remove,
keypress: function (event) {
if (event.which === 32) {
remove();
}
}
}
});
// Load data with new filter
self.loadData();
};
/**
* Update pagination widget.
*
* @param {Number} num size of data collection
*/
H5PDataView.prototype.updatePagination = function (num) {
var self = this;
if (self.pagination === undefined) {
if (self.table === undefined) {
// No table, no pagination
return;
}
// Create new widget
var $pagerContainer = $('<div/>', {'class': 'h5p-pagination'});
self.pagination = new H5PUtils.Pagination(num, self.limit, function (offset) {
// Handle page changes in pagination widget
self.offset = offset;
self.loadData();
}, self.l10n);
self.pagination.appendTo($pagerContainer);
self.table.setFoot($pagerContainer);
}
else {
// Update existing widget
self.pagination.update(num, self.limit);
}
};
/**
* Add filters.
*/
H5PDataView.prototype.addFilters = function () {
var self = this;
for (var i = 0; i < self.filters.length; i++) {
if (self.filters[i] === true) {
// Add text input filter for col i
self.addTextFilter(i);
}
}
};
/**
* Add text filter for given col num.
*
* @param {Number} col
*/
H5PDataView.prototype.addTextFilter = function (col) {
var self = this;
/**
* Find input value and filter on it.
* @private
*/
var search = function () {
var filterOn = $input.val().replace(/^\s+|\s+$/g, '');
if (filterOn === '') {
filterOn = undefined;
}
if (filterOn !== self.filterOn[col]) {
self.filterOn[col] = filterOn;
self.loadData();
}
};
// Add text field for filtering
var typing;
var $input = $('<input/>', {
type: 'text',
placeholder: self.l10n.search,
on: {
'blur': function () {
clearTimeout(typing);
search();
},
'keyup': function (event) {
if (event.keyCode === 13) {
clearTimeout(typing);
search();
return false;
}
else {
clearTimeout(typing);
typing = setTimeout(function () {
search();
}, 500);
}
}
}
}).appendTo(self.$container);
};
/**
* Add toggle for others' H5P content.
* @param {boolean} [checked=false] Initial check setting.
*/
H5PDataView.prototype.addOthersContentToggler = function (checked) {
var self = this;
checked = (typeof checked === 'undefined') ? false : checked;
// Checkbox
this.$othersContentToggler = $('<input/>', {
type: 'checkbox',
'class': 'h5p-others-contents-toggler',
'id': 'h5p-others-contents-toggler',
'checked': checked,
'click': function () {
if ( this.checked ) {
// Add filter on current user
self.filterByFacet( self.columnIdAuthor, H5PIntegration.user.id, H5PIntegration.user.name );
}
else {
// Remove facet indicator and reload full data view
if ( self.facets.hasOwnProperty( self.columnIdAuthor ) && self.facets[self.columnIdAuthor].$tag ) {
self.facets[self.columnIdAuthor].$tag.remove();
}
delete self.facets[self.columnIdAuthor];
self.loadData();
}
}
});
// Label
var $label = $('<label>', {
'class': 'h5p-others-contents-toggler-label',
'text': this.l10n.showOwnContentOnly,
'for': 'h5p-others-contents-toggler'
}).prepend(this.$othersContentToggler);
$('<div>', {
'class': 'h5p-others-contents-toggler-wrapper'
}).append($label)
.appendTo(this.$container);
};
return H5PDataView;
})(H5P.jQuery);

View File

@ -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);

View File

@ -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();

View File

@ -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;
})();

View File

@ -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 = $('<div class="h5p-library-info"></div>');
$.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 = $('<div class="h5p-content empty">' + H5PLibraryDetails.library.translations.noContent + '</div>');
}
else {
H5PLibraryDetails.$content = $('<div class="h5p-content"><h3>' + H5PLibraryDetails.library.translations.contentHeader + '</h3></div>');
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<lastIndex; i++) {
var content = H5PLibraryDetails.currentContent[i];
H5PLibraryDetails.$contentTable.append(H5PUtils.createTableRow(['<a href="' + content.url + '">' + content.title + '</a>']));
}
// 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 = $('<button type="button" class="previous h5p-admin"><</button>');
H5PLibraryDetails.$next = $('<button type="button" class="next h5p-admin">></button>');
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 = $('<span class="pager-info"></span>');
H5PLibraryDetails.$pager = $('<div class="h5p-content-pager"></div>').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 = $('<input/>', {
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 = $('<span/>', {
'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 = $('<div class="h5p-content-search"><input placeholder="' + H5PLibraryDetails.library.translations.filterPlaceholder + '" type="search"></div>');
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 = $('<span class="h5p-admin-search-results">' + H5PLibraryDetails.currentContent.length + ' hits on ' + H5PLibraryDetails.currentFilter + '</span>');
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('<div class="h5p-admin-pager-size-selector">' + H5PLibraryDetails.library.translations.pageSizeSelectorLabel + ':<span data-page-size="10">10</span><span class="selected" data-page-size="20">20</span><span data-page-size="50">50</span><span data-page-size="100">100</span><span data-page-size="200">200</span></div>');
// 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);

View File

@ -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 $('<div>' + t.NA + '</div>');
}
// 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,
'<input class="h5p-admin-restricted" type="checkbox"/>',
{
text: library.numContent,
class: 'h5p-admin-center'
},
{
text: library.numContentDependencies,
class: 'h5p-admin-center'
},
{
text: library.numLibraryDependencies,
class: 'h5p-admin-center'
},
'<div class="h5p-admin-buttons-wrapper">' +
'<button class="h5p-admin-upgrade-library"></button>' +
(library.detailsUrl ? '<button class="h5p-admin-view-library" title="' + t.viewLibrary + '"></button>' : '') +
(library.deleteUrl ? '<button class="h5p-admin-delete-library"></button>' : '') +
'</div>'
]);
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);

View File

@ -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, '*');
}
}
})();

View File

@ -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 = $('<table class="h5p-admin-table' + (H5PAdminIntegration.extraTableClasses !== undefined ? ' ' + H5PAdminIntegration.extraTableClasses : '') + '"></table>');
if (headers) {
var $thead = $('<thead></thead>');
var $tr = $('<tr></tr>');
$.each(headers, function (index, value) {
if (!(value instanceof Object)) {
value = {
html: value
};
}
$('<th/>', 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 <TD>
*/
H5PUtils.createTableRow = function (rows) {
var $tr = $('<tr></tr>');
$.each(rows, function (index, value) {
if (!(value instanceof Object)) {
value = {
html: value
};
}
$('<td/>', 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 = $('<div class="h5p-labeled-field"></div>');
$field.append('<div class="h5p-label">' + label + '</div>');
$field.append('<div class="h5p-value">' + value + '</div>');
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 $('<div/>', {
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 = $('<div class="h5p-admin-rebuild-cache"><p class="message">' + notCached.message + '</p><p class="progress">' + notCached.progress + '</p></div>');
var $button = $('<button>' + notCached.button + '</button>').appendTo($container).click(function () {
var $spinner = $('<div/>', {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 = $('<table/>', tableOptions);
var $thead = $('<thead/>').appendTo($table);
var $tfoot = $('<tfoot/>').appendTo($table);
var $tbody = $('<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 = $('<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 = $('<thead/>');
var $tr = $('<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 = $('<tbody/>');
for (var i = 0; i < rows.length; i++) {
var $tr = $('<tr/>').appendTo($newTbody);
for (var j = 0; j < rows[i].length; j++) {
$('<td>', {
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 = $('<tbody/>');
var $tr = $('<tr/>').appendTo($newTbody);
$('<td>', {
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 = $('<tfoot/>');
var $tr = $('<tr/>').appendTo($newTfoot);
$('<td>', {
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 = $('<button/>', {
html: '&lt;',
'class': 'button',
title: l10n.previousPage
}).click(function () {
goTo(current - 1);
});
// Current page text
var $text = $('<span/>').click(function () {
$input.width($text.width()).show().val(current + 1).focus();
$text.hide();
});
// Jump to page input
var $input = $('<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 = $('<button/>', {
html: '&gt;',
'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);

View File

@ -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;
})();

View File

@ -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'
];

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,436 @@
/**
* Queue requests and handle them at your convenience
*
* @type {RequestQueue}
*/
H5P.RequestQueue = (function ($, EventDispatcher) {
/**
* A queue for requests, will be automatically processed when regaining connection
*
* @param {boolean} [options.showToast] Show toast when losing or regaining connection
* @constructor
*/
const RequestQueue = function (options) {
EventDispatcher.call(this);
this.processingQueue = false;
options = options || {};
this.showToast = options.showToast;
this.itemName = 'requestQueue';
};
/**
* Add request to queue. Only supports posts currently.
*
* @param {string} url
* @param {Object} data
* @returns {boolean}
*/
RequestQueue.prototype.add = function (url, data) {
if (!window.localStorage) {
return false;
}
let storedStatements = this.getStoredRequests();
if (!storedStatements) {
storedStatements = [];
}
storedStatements.push({
url: url,
data: data,
});
window.localStorage.setItem(this.itemName, JSON.stringify(storedStatements));
this.trigger('requestQueued', {
storedStatements: storedStatements,
processingQueue: this.processingQueue,
});
return true;
};
/**
* Get stored requests
*
* @returns {boolean|Array} Stored requests
*/
RequestQueue.prototype.getStoredRequests = function () {
if (!window.localStorage) {
return false;
}
const item = window.localStorage.getItem(this.itemName);
if (!item) {
return [];
}
return JSON.parse(item);
};
/**
* Clear stored requests
*
* @returns {boolean} True if the storage was successfully cleared
*/
RequestQueue.prototype.clearQueue = function () {
if (!window.localStorage) {
return false;
}
window.localStorage.removeItem(this.itemName);
return true;
};
/**
* Start processing of requests queue
*
* @return {boolean} Returns false if it was not possible to resume processing queue
*/
RequestQueue.prototype.resumeQueue = function () {
// Not supported
if (!H5PIntegration || !window.navigator || !window.localStorage) {
return false;
}
// Already processing
if (this.processingQueue) {
return false;
}
// Attempt to send queued requests
const queue = this.getStoredRequests();
const queueLength = queue.length;
// Clear storage, failed requests will be re-added
this.clearQueue();
// No items left in queue
if (!queueLength) {
this.trigger('emptiedQueue', queue);
return true;
}
// Make sure requests are not changed while they're being handled
this.processingQueue = true;
// Process queue in original order
this.processQueue(queue);
return true
};
/**
* Process first item in the request queue
*
* @param {Array} queue Request queue
*/
RequestQueue.prototype.processQueue = function (queue) {
if (!queue.length) {
return;
}
this.trigger('processingQueue');
// Make sure the requests are processed in a FIFO order
const request = queue.shift();
const self = this;
$.post(request.url, request.data)
.fail(self.onQueuedRequestFail.bind(self, request))
.always(self.onQueuedRequestProcessed.bind(self, queue))
};
/**
* Request fail handler
*
* @param {Object} request
*/
RequestQueue.prototype.onQueuedRequestFail = function (request) {
// Queue the failed request again if we're offline
if (!window.navigator.onLine) {
this.add(request.url, request.data);
}
};
/**
* An item in the queue was processed
*
* @param {Array} queue Queue that was processed
*/
RequestQueue.prototype.onQueuedRequestProcessed = function (queue) {
if (queue.length) {
this.processQueue(queue);
return;
}
// Finished processing this queue
this.processingQueue = false;
// Run empty queue callback with next request queue
const requestQueue = this.getStoredRequests();
this.trigger('queueEmptied', requestQueue);
};
/**
* Display toast message on the first content of current page
*
* @param {string} msg Message to display
* @param {boolean} [forceShow] Force override showing the toast
* @param {Object} [configOverride] Override toast message config
*/
RequestQueue.prototype.displayToastMessage = function (msg, forceShow, configOverride) {
if (!this.showToast && !forceShow) {
return;
}
const config = H5P.jQuery.extend(true, {}, {
position: {
horizontal : 'centered',
vertical: 'centered',
noOverflowX: true,
}
}, configOverride);
H5P.attachToastTo(H5P.jQuery('.h5p-content:first')[0], msg, config);
};
return RequestQueue;
})(H5P.jQuery, H5P.EventDispatcher);
/**
* Request queue for retrying failing requests, will automatically retry them when you come online
*
* @type {offlineRequestQueue}
*/
H5P.OfflineRequestQueue = (function (RequestQueue, Dialog) {
/**
* Constructor
*
* @param {Object} [options] Options for offline request queue
* @param {Object} [options.instance] The H5P instance which UI components are placed within
*/
const offlineRequestQueue = function (options) {
const requestQueue = new RequestQueue();
// We could handle requests from previous pages here, but instead we throw them away
requestQueue.clearQueue();
let startTime = null;
const retryIntervals = [10, 20, 40, 60, 120, 300, 600];
let intervalIndex = -1;
let currentInterval = null;
let isAttached = false;
let isShowing = false;
let isLoading = false;
const instance = options.instance;
const offlineDialog = new Dialog({
headerText: H5P.t('offlineDialogHeader'),
dialogText: H5P.t('offlineDialogBody'),
confirmText: H5P.t('offlineDialogRetryButtonLabel'),
hideCancel: true,
hideExit: true,
classes: ['offline'],
instance: instance,
skipRestoreFocus: true,
});
const dialog = offlineDialog.getElement();
// Add retry text to body
const countDownText = document.createElement('div');
countDownText.classList.add('count-down');
countDownText.innerHTML = H5P.t('offlineDialogRetryMessage')
.replace(':num', '<span class="count-down-num">0</span>');
dialog.querySelector('.h5p-confirmation-dialog-text').appendChild(countDownText);
const countDownNum = countDownText.querySelector('.count-down-num');
// Create throbber
const throbberWrapper = document.createElement('div');
throbberWrapper.classList.add('throbber-wrapper');
const throbber = document.createElement('div');
throbber.classList.add('sending-requests-throbber');
throbberWrapper.appendChild(throbber);
requestQueue.on('requestQueued', function (e) {
// Already processing queue, wait until queue has finished processing before showing dialog
if (e.data && e.data.processingQueue) {
return;
}
if (!isAttached) {
const rootContent = document.body.querySelector('.h5p-content');
if (!rootContent) {
return;
}
offlineDialog.appendTo(rootContent);
rootContent.appendChild(throbberWrapper);
isAttached = true;
}
startCountDown();
}.bind(this));
requestQueue.on('queueEmptied', function (e) {
if (e.data && e.data.length) {
// New requests were added while processing queue or requests failed again. Re-queue requests.
startCountDown(true);
return;
}
// Successfully emptied queue
clearInterval(currentInterval);
toggleThrobber(false);
intervalIndex = -1;
if (isShowing) {
offlineDialog.hide();
isShowing = false;
}
requestQueue.displayToastMessage(
H5P.t('offlineSuccessfulSubmit'),
true,
{
position: {
vertical: 'top',
offsetVertical: '100',
}
}
);
}.bind(this));
offlineDialog.on('confirmed', function () {
// Show dialog on next render in case it is being hidden by the 'confirm' button
isShowing = false;
setTimeout(function () {
retryRequests();
}, 100);
}.bind(this));
// Initialize listener for when requests are added to queue
window.addEventListener('online', function () {
retryRequests();
}.bind(this));
// Listen for queued requests outside the iframe
window.addEventListener('message', function (event) {
const isValidQueueEvent = window.parent === event.source
&& event.data.context === 'h5p'
&& event.data.action === 'queueRequest';
if (!isValidQueueEvent) {
return;
}
this.add(event.data.url, event.data.data);
}.bind(this));
/**
* Toggle throbber visibility
*
* @param {boolean} [forceShow] Will force throbber visibility if set
*/
const toggleThrobber = function (forceShow) {
isLoading = !isLoading;
if (forceShow !== undefined) {
isLoading = forceShow;
}
if (isLoading && isShowing) {
offlineDialog.hide();
isShowing = false;
}
if (isLoading) {
throbberWrapper.classList.add('show');
}
else {
throbberWrapper.classList.remove('show');
}
};
/**
* Retries the failed requests
*/
const retryRequests = function () {
clearInterval(currentInterval);
toggleThrobber(true);
requestQueue.resumeQueue();
};
/**
* Increments retry interval
*/
const incrementRetryInterval = function () {
intervalIndex += 1;
if (intervalIndex >= retryIntervals.length) {
intervalIndex = retryIntervals.length - 1;
}
};
/**
* Starts counting down to retrying queued requests.
*
* @param forceDelayedShow
*/
const startCountDown = function (forceDelayedShow) {
// Already showing, wait for retry
if (isShowing) {
return;
}
toggleThrobber(false);
if (!isShowing) {
if (forceDelayedShow) {
// Must force delayed show since dialog may be hiding, and confirmation dialog does not
// support this.
setTimeout(function () {
offlineDialog.show(0);
}, 100);
}
else {
offlineDialog.show(0);
}
}
isShowing = true;
startTime = new Date().getTime();
incrementRetryInterval();
clearInterval(currentInterval);
currentInterval = setInterval(updateCountDown, 100);
};
/**
* Updates the count down timer. Retries requests when time expires.
*/
const updateCountDown = function () {
const time = new Date().getTime();
const timeElapsed = Math.floor((time - startTime) / 1000);
const timeLeft = retryIntervals[intervalIndex] - timeElapsed;
countDownNum.textContent = timeLeft.toString();
// Retry interval reached, retry requests
if (timeLeft <= 0) {
retryRequests();
}
};
/**
* Add request to offline request queue. Only supports posts for now.
*
* @param {string} url The request url
* @param {Object} data The request data
*/
this.add = function (url, data) {
// Only queue request if it failed because we are offline
if (window.navigator.onLine) {
return false;
}
requestQueue.add(url, data);
};
};
return offlineRequestQueue;
})(H5P.RequestQueue, H5P.ConfirmationDialog);

View File

@ -0,0 +1,68 @@
/* global H5PDisableHubData */
/**
* Global data for disable hub functionality
*
* @typedef {object} H5PDisableHubData Data passed in from the backend
*
* @property {string} selector Selector for the disable hub check-button
* @property {string} overlaySelector Selector for the element that the confirmation dialog will mask
* @property {Array} errors Errors found with the current server setup
*
* @property {string} header Header of the confirmation dialog
* @property {string} confirmationDialogMsg Body of the confirmation dialog
* @property {string} cancelLabel Cancel label of the confirmation dialog
* @property {string} confirmLabel Confirm button label of the confirmation dialog
*
*/
/**
* Utility that makes it possible to force the user to confirm that he really
* wants to use the H5P hub without proper server settings.
*/
(function ($) {
$(document).on('ready', function () {
// No data found
if (!H5PDisableHubData) {
return;
}
// No errors found, no need for confirmation dialog
if (!H5PDisableHubData.errors || !H5PDisableHubData.errors.length) {
return;
}
H5PDisableHubData.selector = H5PDisableHubData.selector ||
'.h5p-settings-disable-hub-checkbox';
H5PDisableHubData.overlaySelector = H5PDisableHubData.overlaySelector ||
'.h5p-settings-container';
var dialogHtml = '<div>' +
'<p>' + H5PDisableHubData.errors.join('</p><p>') + '</p>' +
'<p>' + H5PDisableHubData.confirmationDialogMsg + '</p>';
// Create confirmation dialog, make sure to include translations
var confirmationDialog = new H5P.ConfirmationDialog({
headerText: H5PDisableHubData.header,
dialogText: dialogHtml,
cancelText: H5PDisableHubData.cancelLabel,
confirmText: H5PDisableHubData.confirmLabel
}).appendTo($(H5PDisableHubData.overlaySelector).get(0));
confirmationDialog.on('confirmed', function () {
enableButton.get(0).checked = true;
});
confirmationDialog.on('canceled', function () {
enableButton.get(0).checked = false;
});
var enableButton = $(H5PDisableHubData.selector);
enableButton.change(function () {
if ($(this).is(':checked')) {
confirmationDialog.show(enableButton.offset().top);
}
});
});
})(H5P.jQuery);

View File

@ -0,0 +1,35 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* Handle display options included in the URL and put them in the H5PIntegration object if it exists.
*/
if (window.H5PIntegration && window.H5PIntegration.contents && location.search) {
var contentData = window.H5PIntegration.contents[Object.keys(window.H5PIntegration.contents)[0]];
if (contentData) {
contentData.displayOptions = contentData.displayOptions || {};
var search = location.search.replace(/^\?/, ''),
split = search.split('&');
split.forEach(function(param) {
var nameAndValue = param.split('=');
if (nameAndValue.length == 2) {
contentData.displayOptions[nameAndValue[0]] = nameAndValue[1] === '1' || nameAndValue[1] === 'true';
}
});
}
}

View File

@ -0,0 +1,155 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/* global H5PEmbedCommunicator:true */
/**
* When embedded the communicator helps talk to the parent page.
* This is a copy of the H5P.communicator, which we need to communicate in this context
*
* @type {H5PEmbedCommunicator}
* @module core_h5p
* @copyright 2019 Joubel AS <contact@joubel.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
H5PEmbedCommunicator = (function() {
/**
* @class
* @private
*/
function Communicator() {
var self = this;
// Maps actions to functions.
var actionHandlers = {};
// Register message listener.
window.addEventListener('message', function receiveMessage(event) {
if (window.parent !== event.source || event.data.context !== 'h5p') {
return; // Only handle messages from parent and in the correct context.
}
if (actionHandlers[event.data.action] !== undefined) {
actionHandlers[event.data.action](event.data);
}
}, false);
/**
* Register action listener.
*
* @param {string} action What you are waiting for
* @param {function} handler What you want done
*/
self.on = function(action, handler) {
actionHandlers[action] = handler;
};
/**
* Send a message to the all mighty father.
*
* @param {string} action
* @param {Object} [data] payload
*/
self.send = function(action, data) {
if (data === undefined) {
data = {};
}
data.context = 'h5p';
data.action = action;
// Parent origin can be anything.
window.parent.postMessage(data, '*');
};
}
return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
})();
document.onreadystatechange = function() {
// Wait for instances to be initialize.
if (document.readyState !== 'complete') {
return;
}
// Check for H5P iFrame.
var iFrame = document.querySelector('.h5p-iframe');
if (!iFrame || !iFrame.contentWindow) {
return;
}
var H5P = iFrame.contentWindow.H5P;
// Check for H5P instances.
if (!H5P || !H5P.instances || !H5P.instances[0]) {
return;
}
var resizeDelay;
var instance = H5P.instances[0];
var parentIsFriendly = false;
// Handle that the resizer is loaded after the iframe.
H5PEmbedCommunicator.on('ready', function() {
H5PEmbedCommunicator.send('hello');
});
// Handle hello message from our parent window.
H5PEmbedCommunicator.on('hello', function() {
// Initial setup/handshake is done.
parentIsFriendly = true;
// Hide scrollbars for correct size.
iFrame.contentDocument.body.style.overflow = 'hidden';
document.body.classList.add('h5p-resizing');
// Content need to be resized to fit the new iframe size.
H5P.trigger(instance, 'resize');
});
// When resize has been prepared tell parent window to resize.
H5PEmbedCommunicator.on('resizePrepared', function() {
H5PEmbedCommunicator.send('resize', {
scrollHeight: iFrame.contentDocument.body.scrollHeight
});
});
H5PEmbedCommunicator.on('resize', function() {
H5P.trigger(instance, 'resize');
});
H5P.on(instance, 'resize', function() {
if (H5P.isFullscreen) {
return; // Skip iframe resize.
}
// Use a delay to make sure iframe is resized to the correct size.
clearTimeout(resizeDelay);
resizeDelay = setTimeout(function() {
// Only resize if the iframe can be resized.
if (parentIsFriendly) {
H5PEmbedCommunicator.send('prepareResize',
{
scrollHeight: iFrame.contentDocument.body.scrollHeight,
clientHeight: iFrame.contentDocument.body.clientHeight
}
);
} else {
H5PEmbedCommunicator.send('hello');
}
}, 0);
});
// Trigger initial resize for instance.
H5P.trigger(instance, 'resize');
};

View File

@ -0,0 +1,358 @@
/* Administration interface styling */
.h5p-content {
border: 1px solid #DDD;
border-radius: 3px;
padding: 10px;
}
.h5p-admin-table,
.h5p-admin-table > tbody {
border: none;
width: 100%;
}
.h5p-admin-table tr:nth-child(odd),
.h5p-data-view tr:nth-child(odd) {
background-color: #F9F9F9;
}
.h5p-admin-table tbody tr:hover {
background-color: #EEE;
}
.h5p-admin-table.empty {
padding: 1em;
background-color: #EEE;
font-size: 1.2em;
font-weight: bold;
}
.h5p-admin-table.libraries th:last-child,
.h5p-admin-table.libraries td:last-child {
text-align: right;
}
.h5p-admin-buttons-wrapper {
white-space: nowrap;
}
.h5p-admin-table.libraries button {
font-size: 2em;
cursor: pointer;
border: 1px solid #AAA;
border-radius: .2em;
background-color: #e0e0e0;
text-shadow: 0 0 0.5em #fff;
padding: 0;
line-height: 1em;
width: 1.125em;
height: 1.05em;
text-indent: -0.125em;
margin: 0.125em 0.125em 0 0.125em;
}
.h5p-admin-upgrade-library:before {
font-family: 'H5P';
content: "\e888";
}
.h5p-admin-view-library:before {
font-family: 'H5P';
content: "\e889";
}
.h5p-admin-delete-library:before {
font-family: 'H5P';
content: "\e890";
}
.h5p-admin-table.libraries button:hover {
background-color: #d0d0d0;
}
.h5p-admin-table.libraries button:disabled:hover {
background-color: #e0e0e0;
cursor: default;
}
.h5p-admin-upgrade-library {
color: #339900;
}
.h5p-admin-view-library {
color: #0066cc;
}
.h5p-admin-delete-library {
color: #990000;
}
.h5p-admin-delete-library:disabled,
.h5p-admin-upgrade-library:disabled {
cursor: default;
color: #c0c0c0;
}
.h5p-library-info {
padding: 1em 1em;
margin: 1em 0;
width: 350px;
border: 1px solid #DDD;
border-radius: 3px;
}
/* Labeled field (label + value) */
.h5p-labeled-field {
border-bottom: 1px solid #ccc;
}
.h5p-labeled-field:last-child {
border-bottom: none;
}
.h5p-labeled-field .h5p-label {
display: inline-block;
min-width: 150px;
font-size: 1.2em;
font-weight: bold;
padding: 0.2em;
}
.h5p-labeled-field .h5p-value {
display: inline-block;
padding: 0.2em;
}
/* Search element */
.h5p-content-search {
display: inline-block;
position: relative;
width: 100%;
padding: 5px 0;
margin-top: 10px;
border: 1px solid #CCC;
border-radius: 3px;
box-shadow: 2px 2px 5px #888888;
}
.h5p-content-search:before {
font-family: 'H5P';
vertical-align: bottom;
content: "\e88a";
font-size: 2em;
line-height: 1.25em;
}
.h5p-content-search input {
font-size: 120%;
line-height: 120%;
}
.h5p-admin-search-results {
margin-left: 10px;
color: #888;
}
.h5p-admin-pager-size-selector {
position: absolute;
right: 10px;
top: .75em;
display: inline-block;
}
.h5p-admin-pager-size-selector > span {
padding: 5px;
margin-left: 10px;
cursor: pointer;
border: 1px solid #CCC;
border-radius: 3px;
}
.h5p-admin-pager-size-selector > span.selected {
background-color: #edf5fa;
}
.h5p-admin-pager-size-selector > span:hover {
background-color: #555;
color: #FFF;
}
/* Generic "javascript"-action button */
button.h5p-admin {
border: 1px solid #AAA;
border-radius: 5px;
padding: 3px 10px;
background-color: #EEE;
cursor: pointer;
display: inline-block;
text-align: center;
color: #222;
}
button.h5p-admin:hover {
background-color: #555;
color: #FFF;
}
button.h5p-admin.disabled,
button.h5p-admin.disabled:hover {
cursor: auto;
color: #CCC;
background-color: #FFF;
}
/* Pager element */
.h5p-content-pager {
display: inline-block;
border: 1px solid #CCC;
border-radius: 3px;
box-shadow: 2px 2px 5px #888888;
width: 100%;
text-align: center;
padding: 3px 0;
}
.h5p-content-pager > button {
min-width: 80px;
font-size: 130%;
line-height: 130%;
border: none;
background: none;
font-family: 'H5P';
font-size: 1.4em;
}
.h5p-content-pager > button:focus {
outline: 0;
}
.h5p-content-pager > button:last-child {
margin-left: 10px;
}
.h5p-content-pager > .pager-info {
cursor: pointer;
padding: 5px;
border-radius: 3px;
}
.h5p-content-pager > .pager-info:hover {
background-color: #555;
color: #FFF;
}
.h5p-content-pager > .pager-info,
.h5p-content-pager > .h5p-pager-goto {
margin: 0 10px;
line-height: 130%;
display: inline-block;
}
.h5p-admin-header {
margin-top: 1.5em;
}
#h5p-library-upload-form.h5p-admin-upload-libraries-form,
#h5p-content-type-cache-update-form.h5p-admin-upload-libraries-form {
position: relative;
margin: 0;
}
.h5p-admin-upload-libraries-form .form-submit {
position: absolute;
top: 0;
right: 0;
}
.h5p-spinner {
padding: 0 0.5em;
font-size: 1.5em;
font-weight: bold;
}
#h5p-admin-container .h5p-admin-center {
text-align: center;
}
.h5p-pagination {
text-align: center;
}
.h5p-pagination > span, .h5p-pagination > input {
margin: 0 1em;
}
.h5p-data-view input[type="text"] {
margin-bottom: 0.5em;
margin-right: 0.5em;
float: left;
}
.h5p-data-view input[type="text"]::-ms-clear {
display: none;
}
.h5p-data-view .h5p-others-contents-toggler-wrapper {
float: right;
line-height: 2;
margin-right: 0.5em;
}
.h5p-data-view .h5p-others-contents-toggler-label {
font-size: 14px;
}
.h5p-data-view .h5p-others-contents-toggler {
margin-right: 0.5em;
}
.h5p-data-view th[role="button"] {
cursor: pointer;
}
.h5p-data-view th[role="button"].h5p-sort:after,
.h5p-data-view th[role="button"]:hover:after,
.h5p-data-view th[role="button"].h5p-sort.h5p-reverse:hover:after {
content: "\25BE";
position: relative;
left: 0.5em;
top: -1px;
}
.h5p-data-view th[role="button"].h5p-sort.h5p-reverse:after,
.h5p-data-view th[role="button"].h5p-sort:hover:after {
content: "\25B4";
top: -2px;
}
.h5p-data-view th[role="button"]:hover:after,
.h5p-data-view th[role="button"].h5p-sort.h5p-reverse:hover:after,
.h5p-data-view th[role="button"].h5p-sort:hover:after {
color: #999;
}
.h5p-data-view .h5p-facet {
cursor: pointer;
color: #0073aa;
outline: none;
}
.h5p-data-view .h5p-facet:hover,
.h5p-data-view .h5p-facet:active {
color: #00a0d2;
}
.h5p-data-view .h5p-facet:focus {
color: #124964;
box-shadow: 0 0 0 1px #5b9dd9,0 0 2px 1px rgba(30,140,190,.8);
}
.h5p-data-view .h5p-facet-wrapper {
line-height: 23px;
}
.h5p-data-view .h5p-facet-tag {
margin: 2px 0 0 0.5em;
font-size: 12px;
background: #e8e8e8;
border: 1px solid #cbcbcc;
border-radius: 5px;
color: #5d5d5d;
padding: 0 24px 0 10px;
display: inline-block;
position: relative;
}
.h5p-data-view .h5p-facet-tag > span {
position: absolute;
right: 0;
top: auto;
bottom: auto;
font-size: 18px;
color: #a2a2a2;
outline: none;
width: 21px;
text-indent: 4px;
letter-spacing: 10px;
overflow: hidden;
cursor: pointer;
}
.h5p-data-view .h5p-facet-tag > span:before {
content: "×";
font-weight: bold;
}
.h5p-data-view .h5p-facet-tag > span:hover,
.h5p-data-view .h5p-facet-tag > span:focus {
color: #a20000;
}
.h5p-data-view .h5p-facet-tag > span:active {
color: #d20000;
}
.content-upgrade-log {
color: red;
}

View File

@ -0,0 +1,183 @@
.h5p-confirmation-dialog-background {
position: fixed;
height: 100%;
width: 100%;
left: 0;
top: 0;
background: rgba(44, 44, 44, 0.9);
opacity: 1;
visibility: visible;
-webkit-transition: opacity 0.1s, linear 0s, visibility 0s linear 0s;
transition: opacity 0.1s linear 0s, visibility 0s linear 0s;
z-index: 201;
}
.h5p-confirmation-dialog-background.hidden {
display: none;
}
.h5p-confirmation-dialog-background.hiding {
opacity: 0;
visibility: hidden;
-webkit-transition: opacity 0.1s, linear 0s, visibility 0s linear 0.1s;
transition: opacity 0.1s linear 0s, visibility 0s linear 0.1s;
}
.h5p-confirmation-dialog-popup:focus {
outline: none;
}
.h5p-confirmation-dialog-popup {
position: absolute;
display: flex;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
max-width: 35em;
min-width: 25em;
top: 2em;
left: 50%;
-webkit-transform: translate(-50%, 0%);
-ms-transform: translate(-50%, 0%);
transform: translate(-50%, 0%);
color: #555;
box-shadow: 0 0 6px 6px rgba(10,10,10,0.3);
-webkit-transition: transform 0.1s ease-in;
transition: transform 0.1s ease-in;
}
.h5p-confirmation-dialog-popup.hidden {
-webkit-transform: translate(-50%, 50%);
-ms-transform: translate(-50%, 50%);
transform: translate(-50%, 50%);
}
.h5p-confirmation-dialog-header {
padding: 1.5em;
background: #fff;
color: #356593;
}
.h5p-confirmation-dialog-header-text {
font-size: 1.25em;
}
.h5p-confirmation-dialog-body {
background: #fafbfc;
border-top: solid 1px #dde0e9;
padding: 1.25em 1.5em;
}
.h5p-confirmation-dialog-text {
margin-bottom: 1.5em;
}
.h5p-confirmation-dialog-buttons {
float: right;
}
button.h5p-confirmation-dialog-exit:visited,
button.h5p-confirmation-dialog-exit:link,
button.h5p-confirmation-dialog-exit {
position: absolute;
background: none;
border: none;
font-size: 2.5em;
top: -0.9em;
right: -1.15em;
color: #fff;
cursor: pointer;
text-decoration: none;
}
button.h5p-confirmation-dialog-exit:focus,
button.h5p-confirmation-dialog-exit:hover {
color: #E4ECF5;
}
.h5p-confirmation-dialog-exit:before {
font-family: "H5P";
content: "\e890";
}
.h5p-core-button.h5p-confirmation-dialog-confirm-button {
padding-left: 0.75em;
margin-bottom: 0;
}
.h5p-core-button.h5p-confirmation-dialog-confirm-button:before {
content: "\e601";
margin-top: -6px;
display: inline-block;
}
.h5p-confirmation-dialog-popup.offline .h5p-confirmation-dialog-buttons {
float: none;
text-align: center;
}
.h5p-confirmation-dialog-popup.offline .count-down {
font-family: Arial;
margin-top: 0.15em;
color: #000;
}
.h5p-confirmation-dialog-popup.offline .h5p-confirmation-dialog-confirm-button:before {
content: "\e90b";
font-weight: normal;
vertical-align: text-bottom;
}
.throbber-wrapper {
display: none;
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
z-index: 1;
background: rgba(44, 44, 44, 0.9);
}
.throbber-wrapper.show {
display: block;
}
.throbber-wrapper .throbber-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.throbber-wrapper .sending-requests-throbber{
position: absolute;
top: 7em;
left: 50%;
transform: translateX(-50%);
}
.throbber-wrapper .sending-requests-throbber:before {
display: block;
font-family: 'H5P';
content: "\e90b";
color: white;
font-size: 10em;
animation: request-throbber 1.5s infinite linear;
}
@keyframes request-throbber {
from {
transform: rotate(0);
}
to {
transform: rotate(359deg);
}
}

View File

@ -0,0 +1,60 @@
button.h5p-core-button:visited,
button.h5p-core-button:link,
button.h5p-core-button {
font-family: "Open Sans", sans-serif;
font-weight: 600;
font-size: 1em;
line-height: 1.2;
padding: 0.5em 1.25em;
border-radius: 2em;
background: #2579c6;
color: #fff;
cursor: pointer;
border: none;
box-shadow: none;
outline: none;
display: inline-block;
text-align: center;
text-shadow: none;
vertical-align: baseline;
text-decoration: none;
-webkit-transition: initial;
transition: initial;
}
button.h5p-core-button:focus {
background: #1f67a8;
}
button.h5p-core-button:hover {
background: rgba(31, 103, 168, 0.83);
}
button.h5p-core-button:active {
background: #104888;
}
button.h5p-core-button:before {
font-family: 'H5P';
padding-right: 0.15em;
font-size: 1.5em;
vertical-align: middle;
line-height: 0.7;
}
button.h5p-core-cancel-button:visited,
button.h5p-core-cancel-button:link,
button.h5p-core-cancel-button {
border: none;
background: none;
color: #a00;
margin-right: 1em;
font-size: 1em;
text-decoration: none;
cursor: pointer;
}
button.h5p-core-cancel-button:hover,
button.h5p-core-cancel-button:focus {
background: none;
border: none;
color: #e40000;
}

View File

@ -0,0 +1,566 @@
/* General CSS for H5P. Licensed under the MIT License.*/
/* Custom H5P font to use for icons. */
@font-face {
font-family: 'h5p';
src: url('../fonts/h5p-core-23.eot?mz1lkp');
src: url('../fonts/h5p-core-23.eot?mz1lkp#iefix') format('embedded-opentype'),
url('../fonts/h5p-core-23.ttf?mz1lkp') format('truetype'),
url('../fonts/h5p-core-23.woff?mz1lkp') format('woff'),
url('../fonts/h5p-core-23.svg?mz1lkp#h5p') format('svg');
font-weight: normal;
font-style: normal;
}
html.h5p-iframe, html.h5p-iframe > body {
font-family: Sans-Serif; /* Use the browser's default sans-serif font. (Since Heletica doesn't look nice on Windows, and Arial on OS X.) */
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.h5p-semi-fullscreen, .h5p-fullscreen, html.h5p-iframe .h5p-container {
overflow: hidden;
}
.h5p-content {
position: relative;
background: #fefefe;
border: 1px solid #EEE;
border-bottom: none;
box-sizing: border-box;
-moz-box-sizing: border-box;
}
.h5p-noselect
{
-khtml-user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
}
html.h5p-iframe .h5p-content {
font-size: 16px;
line-height: 1.5em;
width: 100%;
height: auto;
}
html.h5p-iframe .h5p-fullscreen .h5p-content,
html.h5p-iframe .h5p-semi-fullscreen .h5p-content {
height: 100%;
}
.h5p-content.h5p-no-frame,
.h5p-fullscreen .h5p-content,
.h5p-semi-fullscreen .h5p-content {
border: 0;
}
.h5p-container {
position: relative;
z-index: 1;
}
.h5p-iframe-wrapper.h5p-fullscreen {
background-color: #000;
}
body.h5p-semi-fullscreen {
position: fixed;
width: 100%;
height: 100%;
}
.h5p-container.h5p-semi-fullscreen {
position: fixed;
top: 0;
left: 0;
z-index: 101;
width: 100%;
height: 100%;
background-color: #FFF;
}
.h5p-content-controls {
margin: 0;
position: absolute;
right: 0;
top: 0;
z-index: 3;
}
.h5p-fullscreen .h5p-content-controls {
display: none;
}
.h5p-content-controls > a:link, .h5p-content-controls > a:visited, a.h5p-disable-fullscreen:link, a.h5p-disable-fullscreen:visited {
color: #e5eef6;
}
.h5p-enable-fullscreen:before {
font-family: 'H5P';
content: "\e88c";
}
.h5p-disable-fullscreen:before {
font-family: 'H5P';
content: "\e891";
}
.h5p-enable-fullscreen, .h5p-disable-fullscreen {
cursor: pointer;
color: #EEE;
background: rgb(0,0,0);
background: rgba(0,0,0,0.3);
line-height: 0.975em;
font-size: 2em;
width: 1.125em;
height: 1em;
text-indent: 0.04em;
}
.h5p-disable-fullscreen {
line-height: 0.925em;
width: 1.1em;
height: 0.9em;
}
.h5p-enable-fullscreen:focus,
.h5p-disable-fullscreen:focus {
outline-style: solid;
outline-width: 1px;
outline-offset: 0.25em;
}
.h5p-enable-fullscreen:hover, .h5p-disable-fullscreen:hover {
background: rgba(0,0,0,0.5);
}
.h5p-semi-fullscreen .h5p-enable-fullscreen {
display: none;
}
div.h5p-fullscreen {
width: 100%;
height: 100%;
}
.h5p-iframe-wrapper {
width: auto;
height: auto;
}
.h5p-fullscreen .h5p-iframe-wrapper,
.h5p-semi-fullscreen .h5p-iframe-wrapper {
width: 100%;
height: 100%;
}
.h5p-iframe-wrapper.h5p-semi-fullscreen {
width: auto;
height: auto;
background: black;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100001;
}
.h5p-iframe-wrapper.h5p-semi-fullscreen .buttons {
position: absolute;
top: 0;
right: 0;
z-index: 20;
}
.h5p-iframe-wrapper iframe.h5p-iframe {
/* Hack for IOS landscape / portrait */
width: 10px;
min-width: 100%;
*width: 100%;
/* End of hack */
height: 100%;
z-index: 10;
overflow: hidden;
border: 0;
display: block;
}
.h5p-content ul.h5p-actions {
box-sizing: border-box;
-moz-box-sizing: border-box;
overflow: hidden;
list-style: none;
padding: 0px 10px;
margin: 0;
height: 25px;
font-size: 12px;
background: #FAFAFA;
border-top: 1px solid #EEE;
border-bottom: 1px solid #EEE;
clear: both;
font-family: Sans-Serif;
}
.h5p-fullscreen .h5p-actions, .h5p-semi-fullscreen .h5p-actions {
display: none;
}
.h5p-actions > .h5p-button {
float: left;
cursor: pointer;
margin: 0 0.5em 0 0;
background: none;
padding: 0 0.75em 0 0.25em;
vertical-align: top;
color: #999;
text-decoration: none;
outline: none;
line-height: 23px;
}
.h5p-actions > .h5p-button:hover {
color: #666;
}
.h5p-actions > .h5p-button:active,
.h5p-actions > .h5p-button:focus,
.h5p-actions .h5p-link:active,
.h5p-actions .h5p-link:focus {
color: #666;
}
.h5p-actions > .h5p-button:focus,
.h5p-actions .h5p-link:focus {
outline-style: solid;
outline-width: thin;
outline-offset: -2px;
outline-color: #9ecaed;
}
.h5p-actions > .h5p-button:before {
font-family: 'H5P';
font-size: 20px;
line-height: 20px;
vertical-align: top;
padding-right: 0;
}
.h5p-actions > .h5p-button.h5p-export:before {
content: "\e90b";
}
.h5p-actions > .h5p-button.h5p-copyrights:before {
content: "\e88f";
}
.h5p-actions > .h5p-button.h5p-embed:before {
content: "\e892";
}
.h5p-actions .h5p-link {
float: right;
margin-right: 0;
font-size: 2.0em;
line-height: 23px;
overflow: hidden;
color: #999;
text-decoration: none;
outline: none;
}
.h5p-actions .h5p-link:before {
font-family: 'H5P';
content: "\e88e";
vertical-align: bottom;
}
.h5p-actions > li {
margin: 0;
list-style: none;
}
.h5p-popup-dialog {
position: absolute;
top: 0;
left: 0;
width: 100%;
min-height: 100%;
z-index: 100;
padding: 2em;
box-sizing: border-box;
-moz-box-sizing: border-box;
opacity: 0;
-webkit-transition: opacity 0.2s;
-moz-transition: opacity 0.2s;
-o-transition: opacity 0.2s;
transition: opacity 0.2s;
background:#000;
background:rgba(0,0,0,0.75);
}
.h5p-popup-dialog.h5p-open {
opacity: 1;
}
.h5p-popup-dialog .h5p-inner {
box-sizing: border-box;
-moz-box-sizing: border-box;
background: #fff;
height: 100%;
max-height: 100%;
position: relative;
}
.h5p-popup-dialog .h5p-inner > h2 {
position: absolute;
box-sizing: border-box;
-moz-box-sizing: border-box;
width: 100%;
margin: 0;
background: #eee;
display: block;
color: #656565;
font-size: 1.25em;
padding: 0.325em 0.5em 0.25em;
line-height: 1.25em;
border-bottom: 1px solid #ccc;
z-index: 2;
}
.h5p-popup-dialog .h5p-inner > h2 > a {
font-size: 12px;
margin-left: 1em;
}
.h5p-embed-dialog .h5p-inner,
.h5p-reuse-dialog .h5p-inner,
.h5p-content-user-data-reset-dialog .h5p-inner {
min-width: 316px;
max-width: 400px;
left: 50%;
top: 50%;
transform: translateX(-50%);
}
.h5p-embed-dialog .h5p-embed-code-container,
.h5p-embed-size {
resize: none;
outline: none;
width: 100%;
padding: 0.375em 0.5em 0.25em;
margin: 0;
overflow: hidden;
border: 1px solid #ccc;
box-shadow: 0 1px 2px 0 #d0d0d0 inset;
font-size: 0.875em;
letter-spacing: 0.065em;
font-family: sans-serif;
white-space: pre;
line-height: 1.5em;
height: 2.0714em;
background: #f5f5f5;
box-sizing: border-box;
-moz-box-sizing: border-box;
}
.h5p-embed-dialog .h5p-embed-code-container:focus {
height: 5em;
}
.h5p-embed-size {
width: 3.5em;
text-align: right;
margin: 0.5em 0;
line-height: 2em;
}
.h5p-popup-dialog .h5p-scroll-content {
border-top: 2.25em solid transparent;
padding: 1em;
box-sizing: border-box;
-moz-box-sizing: border-box;
color: #555555;
z-index: 1;
}
.h5p-popup-dialog.h5p-open .h5p-scroll-content {
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
height: 100%;
}
.h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar {
width: 8px;
}
.h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar-track {
background: #e0e0e0;
}
.h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar-thumb {
box-shadow: 0 0 10px #000 inset;
border-radius: 4px;
}
.h5p-popup-dialog .h5p-close {
cursor: pointer;
}
.h5p-popup-dialog .h5p-close:after {
font-family: 'H5P';
content: "\e894";
font-size: 2em;
position: absolute;
right: 0;
top: 0;
width: 1.125em;
height: 1.125em;
line-height: 1.125em;
color: #656565;
cursor: pointer;
text-indent: -0.065em;
z-index: 3
}
.h5p-popup-dialog .h5p-close:hover:after,
.h5p-popup-dialog .h5p-close:focus:after {
color: #454545;
}
.h5p-popup-dialog .h5p-close:active:after {
color: #252525;
}
.h5p-poopup-dialog h2 {
margin: 0.25em 0 0.5em;
}
.h5p-popup-dialog h3 {
margin: 0.75em 0 0.25em;
}
.h5p-popup-dialog dl {
margin: 0.25em 0 0.75em;
}
.h5p-popup-dialog dt {
float: left;
margin: 0 0.75em 0 0;
}
.h5p-popup-dialog dt:after {
content: ':';
}
.h5p-popup-dialog dd {
margin: 0;
}
.h5p-expander {
cursor: pointer;
font-size: 1.125em;
outline: none;
margin: 0.5em 0 0;
display: inline-block;
}
.h5p-expander:before {
content: "+";
width: 1em;
display: inline-block;
font-weight: bold;
}
.h5p-expander.h5p-open:before {
content: "-";
text-indent: 0.125em;
}
.h5p-expander:hover,
.h5p-expander:focus {
color: #303030;
}
.h5p-expander:active {
color: #202020;
}
.h5p-expander-content {
display: none;
}
.h5p-expander-content p {
margin: 0.5em 0;
}
.h5p-content-copyrights {
border-left: 0.25em solid #d0d0d0;
margin-left: 0.25em;
padding-left: 0.25em;
}
.h5p-throbber {
background: url('../images/throbber.gif?ver=1.2.1') 10px center no-repeat;
padding-left: 38px;
min-height: 30px;
line-height: 30px;
}
.h5p-dialog-ok-button {
cursor: default;
float: right;
outline: none;
border: 2px solid #ccc;
padding: 0.25em 0.75em 0.125em;
background: #eee;
}
.h5p-dialog-ok-button:hover,
.h5p-dialog-ok-button:focus {
background: #fafafa;
}
.h5p-dialog-ok-button:active {
background: #eeffee;
}
.h5p-big-button {
line-height: 1.25;
display: block;
position: relative;
cursor: pointer;
width: 100%;
padding: 1em 1em 1em 3.75em;
text-align: left;
border: 1px solid #dedede;
background: linear-gradient(#ffffff, #f1f1f2);
border-radius: 0.25em;
}
.h5p-big-button:before {
font-family: 'h5p';
content: "\e893";
line-height: 1;
font-size: 3em;
color: #2747f7;
position: absolute;
left: 0.125em;
top: 0.125em;
}
.h5p-copy-button:before {
content: "\e905";
}
.h5p-big-button:hover {
border: 1px solid #2747f7;
background: #eff1fe;
}
.h5p-big-button:active {
border: 1px solid #dedede;
background: #dfe4fe;
}
.h5p-button-title {
color: #2747f7;
font-size: 15px;
font-weight: bold;
margin-bottom: 0.5em;
}
.h5p-button-description {
color: #757575;
}
.h5p-horizontal-line-text {
border-top: 1px solid #dadada;
line-height: 1;
color: #474747;
text-align: center;
position: relative;
margin: 1.25em 0;
}
.h5p-horizontal-line-text > span {
background: white;
padding: 0.5em;
position: absolute;
top: -1em;
left: 50%;
transform: translateX(-50%);
}
.h5p-toast {
font-size: 0.75em;
background-color: rgba(0, 0, 0, 0.9);
color: #fff;
z-index: 110;
position: absolute;
padding: 0 0.5em;
line-height: 2;
border-radius: 4px;
white-space: nowrap;
pointer-events: none;
top: 0;
opacity: 1;
visibility: visible;
transition: opacity 1s;
}
.h5p-toast-disabled {
opacity: 0;
visibility: hidden;
}
/* This is loaded as part of Core and not Editor since this needs to be outside the editor iframe */
.h5peditor-semi-fullscreen {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 101;
}
iframe.h5peditor-semi-fullscreen {
background: #fff;
z-index: 100001;
}
.h5p-content.using-mouse *:not(textarea):focus {
outline: none !important;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreH5PPlayerComponent } from './h5p-player/h5p-player';
import { CoreComponentsModule } from '@components/components.module';
@NgModule({
declarations: [
CoreH5PPlayerComponent
],
imports: [
CommonModule,
IonicModule,
CoreDirectivesModule,
TranslateModule.forChild(),
CoreComponentsModule
],
providers: [
],
exports: [
CoreH5PPlayerComponent
],
entryComponents: [
CoreH5PPlayerComponent
]
})
export class CoreH5PComponentsModule {}

View File

@ -0,0 +1,13 @@
<div *ngIf="!showPackage" class="core-h5p-placeholder">
<button *ngIf="!loading" class="core-h5p-placeholder-play-button" ion-button icon-only clear (click)="play($event)">
<core-icon name="fa-play-circle"></core-icon>
</button>
<ion-spinner *ngIf="loading" class="core-h5p-placeholder-spinner"></ion-spinner>
<div class="core-h5p-placeholder-download-container">
<core-download-refresh [status]="state" [enabled]="canDownload" [loading]="calculating" [canTrustDownload]="true" (action)="download()"></core-download-refresh>
</div>
</div>
<core-iframe *ngIf="showPackage" [src]="playerSrc" iframeHeight="auto"></core-iframe>
<script *ngIf="resizeScript && showPackage" type="text/javascript" [src]="resizeScript"></script>

View File

@ -0,0 +1,50 @@
// H5P variables.
$core-h5p-placeholder-bg-color: $gray !default;
$core-h5p-placeholder-text-color: $text-color !default;
ion-app.app-root core-h5p-player {
.core-h5p-placeholder {
position: relative;
width: 100%;
height: 230px;
background: url('../assets/img/icons/h5p.svg') center top 25px / 100px auto no-repeat $core-h5p-placeholder-bg-color;
color: $core-h5p-placeholder-text-color;
.icon {
color: $core-h5p-placeholder-text-color;
}
.core-h5p-placeholder-play-button, .core-h5p-placeholder-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.core-h5p-placeholder-play-button {
font-size: 30px;
min-height: 50px;
}
.core-h5p-placeholder-download-container {
position: absolute;
top: 0;
right: 0;
ion-spinner {
margin-right: 0.75em;
}
core-download-refresh > ion-icon {
margin: 0.4rem 0.2rem;
padding: 0 0.5em;
line-height: .67;
}
}
ion-spinner circle {
stroke: $core-h5p-placeholder-text-color;
}
}
}

View File

@ -0,0 +1,273 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, ElementRef, OnInit, OnDestroy, OnChanges, SimpleChange } from '@angular/core';
import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreFileProvider } from '@providers/file';
import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreH5PProvider } from '@core/h5p/providers/h5p';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
import { CoreFileHelperProvider } from '@providers/file-helper';
import { CoreConstants } from '@core/constants';
import { CoreSite } from '@classes/site';
/**
* Component to render an H5P package.
*/
@Component({
selector: 'core-h5p-player',
templateUrl: 'core-h5p-player.html'
})
export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
@Input() src: string; // The URL of the player to display the H5P package.
@Input() component?: string; // Component.
@Input() componentId?: string | number; // Component ID to use in conjunction with the component.
playerSrc: string;
showPackage = false;
loading = false;
state: string;
canDownload: boolean;
calculating = true;
protected site: CoreSite;
protected siteId: string;
protected siteCanDownload: boolean;
protected observer;
protected urlParams;
protected logger;
constructor(loggerProvider: CoreLoggerProvider,
public elementRef: ElementRef,
protected sitesProvider: CoreSitesProvider,
protected urlUtils: CoreUrlUtilsProvider,
protected utils: CoreUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected h5pProvider: CoreH5PProvider,
protected filepoolProvider: CoreFilepoolProvider,
protected eventsProvider: CoreEventsProvider,
protected appProvider: CoreAppProvider,
protected domUtils: CoreDomUtilsProvider,
protected pluginFileDelegate: CorePluginFileDelegate,
protected fileProvider: CoreFileProvider,
protected fileHelper: CoreFileHelperProvider) {
this.logger = loggerProvider.getInstance('CoreH5PPlayerComponent');
this.site = sitesProvider.getCurrentSite();
this.siteId = this.site.getId();
this.siteCanDownload = this.sitesProvider.getCurrentSite().canDownloadFiles();
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.checkCanDownload();
}
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
// If it's already playing there's no need to check if it can be downloaded.
if (changes.src && !this.showPackage) {
this.checkCanDownload();
}
}
/**
* Play the H5P.
*
* @param e Event.
*/
play(e: MouseEvent): void {
e.preventDefault();
e.stopPropagation();
this.loading = true;
let promise;
this.addResizerScript();
if (this.canDownload && this.fileHelper.isStateDownloaded(this.state)) {
// Package is downloaded, use the local URL.
promise = this.h5pProvider.getContentIndexFileUrl(this.urlParams.url, this.urlParams, this.siteId).catch(() => {
// Index file doesn't exist, probably deleted because a lib was updated. Try to create it again.
return this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.urlParams.url).then((path) => {
return this.fileProvider.getFile(path);
}).then((file) => {
return this.h5pProvider.extractH5PFile(this.urlParams.url, file, this.siteId);
}).then(() => {
// File treated. Try to get the index file URL again.
return this.h5pProvider.getContentIndexFileUrl(this.urlParams.url, this.urlParams, this.siteId);
});
}).catch((error) => {
// Still failing. Delete the H5P package?
this.logger.error('Error loading downloaded index:', error, this.src);
});
} else {
promise = Promise.resolve();
}
promise.then((url) => {
if (url) {
// Local package.
this.playerSrc = url;
} else {
// Never allow downloading in the app. This will only work if the user is allowed to change the params.
const src = this.src && this.src.replace(CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD + '=1',
CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD + '=0');
// Get auto-login URL so the user is automatically authenticated.
return this.sitesProvider.getCurrentSite().getAutoLoginUrl(src, false).then((url) => {
// Add the preventredirect param so the user can authenticate.
this.playerSrc = this.urlUtils.addParamsToUrl(url, {preventredirect: false});
});
}
}).finally(() => {
this.loading = false;
this.showPackage = true;
if (this.canDownload && (this.state == CoreConstants.OUTDATED || this.state == CoreConstants.NOT_DOWNLOADED)) {
// Download the package in background if the size is low.
this.attemptDownloadInBg().catch((error) => {
this.logger.error('Error downloading H5P in background', error);
});
}
});
}
/**
* Download the package.
*/
download(e: Event): void {
e && e.preventDefault();
e && e.stopPropagation();
if (!this.appProvider.isOnline()) {
this.domUtils.showErrorModal('core.networkerrormsg', true);
return;
}
// Get the file size and ask the user to confirm.
this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId).then((size) => {
return this.domUtils.confirmDownloadSize({ size: size, total: true }).then(() => {
// User confirmed, add to the queue.
return this.filepoolProvider.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId);
}, () => {
// User cancelled.
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
this.calculateState();
});
}
/**
* Download the H5P in background if the size is low.
*
* @return Promise resolved when done.
*/
protected attemptDownloadInBg(): Promise<any> {
if (this.urlParams && this.src && this.siteCanDownload && this.h5pProvider.canGetTrustedH5PFileInSite() &&
this.appProvider.isOnline()) {
// Get the file size.
return this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId).then((size) => {
if (this.filepoolProvider.shouldDownload(size)) {
// Download the file in background.
this.filepoolProvider.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId);
}
});
}
return Promise.resolve();
}
/**
* Add the resizer script if it hasn't been added already.
*/
protected addResizerScript(): void {
const script = document.createElement('script');
script.id = 'core-h5p-resizer-script';
script.type = 'text/javascript';
script.src = this.h5pProvider.getResizerScriptUrl();
document.head.appendChild(script);
}
/**
* Check if the package can be downloaded.
*/
protected checkCanDownload(): void {
this.observer && this.observer.off();
this.urlParams = this.urlUtils.extractUrlParams(this.src);
if (this.src && this.siteCanDownload && this.h5pProvider.canGetTrustedH5PFileInSite() && this.site.containsUrl(this.src)) {
this.calculateState();
// Listen for changes in the state.
this.filepoolProvider.getFileEventNameByUrl(this.siteId, this.urlParams.url).then((eventName) => {
this.observer = this.eventsProvider.on(eventName, () => {
this.calculateState();
});
}).catch(() => {
// An error probably means the file cannot be downloaded or we cannot check it (offline).
});
} else {
this.calculating = false;
this.canDownload = false;
}
}
/**
* Calculate state of the file.
*
* @param fileUrl The H5P file URL.
*/
protected calculateState(): void {
this.calculating = true;
// Get the status of the file.
this.filepoolProvider.getFileStateByUrl(this.siteId, this.urlParams.url).then((state) => {
this.canDownload = true;
this.state = state;
}).catch((error) => {
this.canDownload = false;
}).finally(() => {
this.calculating = false;
});
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.observer && this.observer.off();
}
}

View File

@ -0,0 +1,46 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CoreH5PComponentsModule } from './components/components.module';
import { CoreH5PProvider } from './providers/h5p';
import { CoreH5PUtilsProvider } from './providers/utils';
import { CoreH5PPluginFileHandler } from './providers/pluginfile-handler';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
// List of providers (without handlers).
export const CORE_H5P_PROVIDERS: any[] = [
CoreH5PProvider,
CoreH5PUtilsProvider
];
@NgModule({
declarations: [],
imports: [
CoreH5PComponentsModule
],
providers: [
CoreH5PProvider,
CoreH5PUtilsProvider,
CoreH5PPluginFileHandler
],
exports: []
})
export class CoreH5PModule {
constructor(pluginfileDelegate: CorePluginFileDelegate,
pluginfileHandler: CoreH5PPluginFileHandler) {
pluginfileDelegate.registerHandler(pluginfileHandler);
}
}

View File

@ -0,0 +1,91 @@
{
"additionallicenseinfo": "Any additional information about the license",
"author": "Author",
"authorcomments": "Author comments",
"authorcommentsdescription": "Comments for the editor of the content. (This text will not be published as a part of the copyright info.)",
"authorname": "Author's name",
"authorrole": "Author's role",
"by": "by",
"cancellabel": "Cancel",
"ccattribution": "Attribution (CC BY)",
"ccattributionnc": "Attribution-NonCommercial (CC BY-NC)",
"ccattributionncnd": "Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)",
"ccattributionncsa": "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)",
"ccattributionnd": "Attribution-NoDerivs (CC BY-ND)",
"ccattributionsa": "Attribution-ShareAlike (CC BY-SA)",
"ccpdd": "Public Domain Dedication (CC0)",
"changedby": "Changed by",
"changedescription": "Description of change",
"changelog": "Changelog",
"changeplaceholder": "Photo cropped, text changed, etc.",
"close": "Close",
"confirmdialogbody": "Please confirm that you wish to proceed. This action is not reversible.",
"confirmdialogheader": "Confirm action",
"confirmlabel": "Confirm",
"connectionLost": "Connection lost. Results will be stored and sent when you regain connection.",
"connectionReestablished": "Connection reestablished.",
"contentCopied": "Content is copied to the clipboard",
"contentchanged": "This content has changed since you last used it.",
"contenttype": "Content Type",
"copyright": "Rights of use",
"copyrightinfo": "Copyright information",
"copyrightstring": "Copyright",
"copyrighttitle": "View copyright information for this content.",
"creativecommons": "Creative Commons",
"date": "Date",
"disablefullscreen": "Disable fullscreen",
"download": "Download",
"downloadtitle": "Download this content as a H5P file.",
"editor": "Editor",
"embed": "Embed",
"embedtitle": "View the embed code for this content.",
"fullscreen": "Fullscreen",
"gpl": "General Public License v3",
"h5ptitle": "Visit H5P.org to check out more cool content.",
"hideadvanced": "Hide advanced",
"license": "License",
"licenseCC010": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
"licenseCC010U": "CC0 1.0 Universal",
"licenseCC10": "1.0 Generic",
"licenseCC20": "2.0 Generic",
"licenseCC25": "2.5 Generic",
"licenseCC30": "3.0 Unported",
"licenseCC40": "4.0 International",
"licenseGPL": "General Public License",
"licenseV1": "Version 1",
"licenseV2": "Version 2",
"licenseV3": "Version 3",
"licensee": "Licensee",
"licenseextras": "License Extras",
"licenseversion": "License version",
"nocopyright": "No copyright information available for this content.",
"offlineDialogBody": "We were unable to send information about your completion of this task. Please check your internet connection.",
"offlineDialogHeader": "Your connection to the server was lost",
"offlineDialogRetryButtonLabel": "Retry now",
"offlineDialogRetryMessage": "Retrying in :num....",
"offlineSuccessfulSubmit": "Successfully submitted results.",
"originator": "Originator",
"pd": "Public Domain",
"pddl": "Public Domain Dedication and Licence",
"pdm": "Public Domain Mark (PDM)",
"play": "Play H5P",
"resizescript": "Include this script on your website if you want dynamic sizing of the embedded content:",
"resubmitScores": "Attempting to submit stored results.",
"reuse": "Reuse",
"reuseContent": "Reuse Content",
"reuseDescription": "Reuse this content.",
"showadvanced": "Show advanced",
"showless": "Show less",
"showmore": "Show more",
"size": "Size",
"source": "Source",
"startingover": "You'll be starting over.",
"sublevel": "Sublevel",
"thumbnail": "Thumbnail",
"title": "Title",
"undisclosed": "Undisclosed",
"year": "Year",
"years": "Year(s)",
"yearsfrom": "Years (from)",
"yearsto": "Years (to)"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,136 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreFileProvider } from '@providers/file';
import { CorePluginFileHandler } from '@providers/plugin-file-delegate';
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreH5PProvider } from './h5p';
import { CoreWSExternalFile } from '@providers/ws';
import { FileEntry } from '@ionic-native/file';
/**
* Handler to treat H5P files.
*/
@Injectable()
export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
name = 'CoreH5PPluginFileHandler';
constructor(protected urlUtils: CoreUrlUtilsProvider,
protected mimeUtils: CoreMimetypeUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected utils: CoreUtilsProvider,
protected fileProvider: CoreFileProvider,
protected h5pProvider: CoreH5PProvider) { }
/**
* React to a file being deleted.
*
* @param fileUrl The file URL used to download the file.
* @param path The path of the deleted file.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<any> {
// If an h5p file is deleted, remove the contents folder.
return this.h5pProvider.deleteContentByUrl(fileUrl, siteId);
}
/**
* Check whether a file can be downloaded. If so, return the file to download.
*
* @param file The file data.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the file to use. Rejected if cannot download.
*/
getDownloadableFile(file: CoreWSExternalFile, siteId?: string): Promise<CoreWSExternalFile> {
return this.h5pProvider.getTrustedH5PFile(file.fileurl, {}, false, siteId);
}
/**
* Given an HTML element, get the URLs of the files that should be downloaded and weren't treated by
* CoreFilepoolProvider.extractDownloadableFilesFromHtml.
*
* @param container Container where to get the URLs from.
* @return {string[]} List of URLs.
*/
getDownloadableFilesFromHTML(container: HTMLElement): string[] {
const iframes = <HTMLIFrameElement[]> Array.from(container.querySelectorAll('iframe.h5p-iframe'));
const urls = [];
for (let i = 0; i < iframes.length; i++) {
const params = this.urlUtils.extractUrlParams(iframes[i].src);
if (params.url) {
urls.push(params.url);
}
}
return urls;
}
/**
* Get a file size.
*
* @param file The file data.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the size.
*/
getFileSize(file: CoreWSExternalFile, siteId?: string): Promise<number> {
return this.h5pProvider.getTrustedH5PFile(file.fileurl, {}, false, siteId).then((file) => {
return file.filesize;
}).catch((error): any => {
if (this.utils.isWebServiceError(error)) {
// WS returned an error, it means it cannot be downloaded.
return 0;
}
return Promise.reject(error);
});
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.h5pProvider.canGetTrustedH5PFileInSite();
}
/**
* Check whether the file should be treated by this handler. It is used in functions where the component isn't used.
*
* @param file The file data.
* @return Whether the file should be treated by this handler.
*/
shouldHandleFile(file: CoreWSExternalFile): boolean {
return this.mimeUtils.guessExtensionFromUrl(file.fileurl) == 'h5p';
}
/**
* Treat a downloaded file.
*
* @param fileUrl The file URL used to download the file.
* @param file The file entry of the downloaded file.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise<any> {
return this.h5pProvider.extractH5PFile(fileUrl, file, siteId);
}
}

View File

@ -0,0 +1,429 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreH5PContentDependencyData, CoreH5PDependencyAsset } from './h5p';
import { Md5 } from 'ts-md5/dist/md5';
/**
* Utils service with helper functions for H5P.
*/
@Injectable()
export class CoreH5PUtilsProvider {
// Map to slugify characters.
protected SLUGIFY_MAP = {
æ: 'ae',
ø: 'oe',
ö: 'o',
ó: 'o',
ô: 'o',
Ò: 'oe',
Õ: 'o',
Ý: 'o',
ý: 'y',
ÿ: 'y',
ā: 'y',
ă: 'a',
ą: 'a',
œ: 'a',
å: 'a',
ä: 'a',
á: 'a',
à: 'a',
â: 'a',
ã: 'a',
ç: 'c',
ć: 'c',
ĉ: 'c',
ċ: 'c',
č: 'c',
é: 'e',
è: 'e',
ê: 'e',
ë: 'e',
í: 'i',
ì: 'i',
î: 'i',
ï: 'i',
ú: 'u',
ñ: 'n',
ü: 'u',
ù: 'u',
û: 'u',
ß: 'es',
ď: 'd',
đ: 'd',
ē: 'e',
ĕ: 'e',
ė: 'e',
ę: 'e',
ě: 'e',
ĝ: 'g',
ğ: 'g',
ġ: 'g',
ģ: 'g',
ĥ: 'h',
ħ: 'h',
ĩ: 'i',
ī: 'i',
ĭ: 'i',
į: 'i',
ı: 'i',
ij: 'ij',
ĵ: 'j',
ķ: 'k',
ĺ: 'l',
ļ: 'l',
ľ: 'l',
ŀ: 'l',
ł: 'l',
ń: 'n',
ņ: 'n',
ň: 'n',
ʼn: 'n',
ō: 'o',
ŏ: 'o',
ő: 'o',
ŕ: 'r',
ŗ: 'r',
ř: 'r',
ś: 's',
ŝ: 's',
ş: 's',
š: 's',
ţ: 't',
ť: 't',
ŧ: 't',
ũ: 'u',
ū: 'u',
ŭ: 'u',
ů: 'u',
ű: 'u',
ų: 'u',
ŵ: 'w',
ŷ: 'y',
ź: 'z',
ż: 'z',
ž: 'z',
ſ: 's',
ƒ: 'f',
ơ: 'o',
ư: 'u',
ǎ: 'a',
ǐ: 'i',
ǒ: 'o',
ǔ: 'u',
ǖ: 'u',
ǘ: 'u',
ǚ: 'u',
ǜ: 'u',
ǻ: 'a',
ǽ: 'ae',
ǿ: 'oe'
};
constructor(private translate: TranslateService,
private textUtils: CoreTextUtilsProvider) { }
/**
* The metadataSettings field in libraryJson uses 1 for true and 0 for false.
* Here we are converting these to booleans, and also doing JSON encoding.
*
* @param metadataSettings Settings.
* @return Stringified settings.
*/
boolifyAndEncodeMetadataSettings(metadataSettings: any): string {
// Convert metadataSettings values to boolean.
if (typeof metadataSettings.disable != 'undefined') {
metadataSettings.disable = metadataSettings.disable === 1;
}
if (typeof metadataSettings.disableExtraTitleField != 'undefined') {
metadataSettings.disableExtraTitleField = metadataSettings.disableExtraTitleField === 1;
}
return JSON.stringify(metadataSettings);
}
/**
* Determine the correct embed type to use.
*
* @param Embed type of the content.
* @param Embed type of the main library.
* @return Either 'div' or 'iframe'.
*/
determineEmbedType(contentEmbedType: string, libraryEmbedTypes: string): string {
// Detect content embed type.
let embedType = contentEmbedType.toLowerCase().indexOf('div') != -1 ? 'div' : 'iframe';
if (libraryEmbedTypes) {
// Check that embed type is available for library
const embedTypes = libraryEmbedTypes.toLowerCase();
if (embedTypes.indexOf(embedType) == -1) {
// Not available, pick default.
embedType = embedTypes.indexOf('div') != -1 ? 'div' : 'iframe';
}
}
return embedType;
}
/**
* Combines path with version.
*
* @param assets List of assets to get their URLs.
* @param assetsFolderPath The path of the folder where the assets are.
* @return List of urls.
*/
getAssetsUrls(assets: CoreH5PDependencyAsset[], assetsFolderPath: string = ''): string[] {
const urls = [];
assets.forEach((asset) => {
let url = asset.path;
// Add URL prefix if not external.
if (asset.path.indexOf('://') == -1 && assetsFolderPath) {
url = this.textUtils.concatenatePaths(assetsFolderPath, url);
}
// Add version if set.
if (asset.version) {
url += asset.version;
}
urls.push(url);
});
return urls;
}
/**
* Get the hash of a list of dependencies.
*
* @param dependencies Dependencies.
* @return Hash.
*/
getDependenciesHash(dependencies: {[machineName: string]: CoreH5PContentDependencyData}): string {
// Build hash of dependencies.
const toHash = [];
// Use unique identifier for each library version.
for (const name in dependencies) {
const dep = dependencies[name];
toHash.push(dep.machineName + '-' + dep.majorVersion + '.' + dep.minorVersion + '.' + dep.patchVersion);
}
// Sort in case the same dependencies comes in a different order.
toHash.sort((a, b) => {
return a.localeCompare(b);
});
// Calculate hash.
return <string> Md5.hashAsciiStr(toHash.join(''));
}
/**
* Provide localization for the Core JS.
*
* @return Object with the translations.
*/
getLocalization(): any {
return {
fullscreen: this.translate.instant('core.h5p.fullscreen'),
disableFullscreen: this.translate.instant('core.h5p.disablefullscreen'),
download: this.translate.instant('core.h5p.download'),
copyrights: this.translate.instant('core.h5p.copyright'),
embed: this.translate.instant('core.h5p.embed'),
size: this.translate.instant('core.h5p.size'),
showAdvanced: this.translate.instant('core.h5p.showadvanced'),
hideAdvanced: this.translate.instant('core.h5p.hideadvanced'),
advancedHelp: this.translate.instant('core.h5p.resizescript'),
copyrightInformation: this.translate.instant('core.h5p.copyright'),
close: this.translate.instant('core.h5p.close'),
title: this.translate.instant('core.h5p.title'),
author: this.translate.instant('core.h5p.author'),
year: this.translate.instant('core.h5p.year'),
source: this.translate.instant('core.h5p.source'),
license: this.translate.instant('core.h5p.license'),
thumbnail: this.translate.instant('core.h5p.thumbnail'),
noCopyrights: this.translate.instant('core.h5p.nocopyright'),
reuse: this.translate.instant('core.h5p.reuse'),
reuseContent: this.translate.instant('core.h5p.reuseContent'),
reuseDescription: this.translate.instant('core.h5p.reuseDescription'),
downloadDescription: this.translate.instant('core.h5p.downloadtitle'),
copyrightsDescription: this.translate.instant('core.h5p.copyrighttitle'),
embedDescription: this.translate.instant('core.h5p.embedtitle'),
h5pDescription: this.translate.instant('core.h5p.h5ptitle'),
contentChanged: this.translate.instant('core.h5p.contentchanged'),
startingOver: this.translate.instant('core.h5p.startingover'),
by: this.translate.instant('core.h5p.by'),
showMore: this.translate.instant('core.h5p.showmore'),
showLess: this.translate.instant('core.h5p.showless'),
subLevel: this.translate.instant('core.h5p.sublevel'),
confirmDialogHeader: this.translate.instant('core.h5p.confirmdialogheader'),
confirmDialogBody: this.translate.instant('core.h5p.confirmdialogbody'),
cancelLabel: this.translate.instant('core.h5p.cancellabel'),
confirmLabel: this.translate.instant('core.h5p.confirmlabel'),
licenseU: this.translate.instant('core.h5p.undisclosed'),
licenseCCBY: this.translate.instant('core.h5p.ccattribution'),
licenseCCBYSA: this.translate.instant('core.h5p.ccattributionsa'),
licenseCCBYND: this.translate.instant('core.h5p.ccattributionnd'),
licenseCCBYNC: this.translate.instant('core.h5p.ccattributionnc'),
licenseCCBYNCSA: this.translate.instant('core.h5p.ccattributionncsa'),
licenseCCBYNCND: this.translate.instant('core.h5p.ccattributionncnd'),
licenseCC40: this.translate.instant('core.h5p.licenseCC40'),
licenseCC30: this.translate.instant('core.h5p.licenseCC30'),
licenseCC25: this.translate.instant('core.h5p.licenseCC25'),
licenseCC20: this.translate.instant('core.h5p.licenseCC20'),
licenseCC10: this.translate.instant('core.h5p.licenseCC10'),
licenseGPL: this.translate.instant('core.h5p.licenseGPL'),
licenseV3: this.translate.instant('core.h5p.licenseV3'),
licenseV2: this.translate.instant('core.h5p.licenseV2'),
licenseV1: this.translate.instant('core.h5p.licenseV1'),
licensePD: this.translate.instant('core.h5p.pd'),
licenseCC010: this.translate.instant('core.h5p.licenseCC010'),
licensePDM: this.translate.instant('core.h5p.pdm'),
licenseC: this.translate.instant('core.h5p.copyrightstring'),
contentType: this.translate.instant('core.h5p.contenttype'),
licenseExtras: this.translate.instant('core.h5p.licenseextras'),
changes: this.translate.instant('core.h5p.changelog'),
contentCopied: this.translate.instant('core.h5p.contentCopied'),
connectionLost: this.translate.instant('core.h5p.connectionLost'),
connectionReestablished: this.translate.instant('core.h5p.connectionReestablished'),
resubmitScores: this.translate.instant('core.h5p.resubmitScores'),
offlineDialogHeader: this.translate.instant('core.h5p.offlineDialogHeader'),
offlineDialogBody: this.translate.instant('core.h5p.offlineDialogBody'),
offlineDialogRetryMessage: this.translate.instant('core.h5p.offlineDialogRetryMessage'),
offlineDialogRetryButtonLabel: this.translate.instant('core.h5p.offlineDialogRetryButtonLabel'),
offlineSuccessfulSubmit: this.translate.instant('core.h5p.offlineSuccessfulSubmit'),
};
}
/**
* Parses library data from a string on the form {machineName} {majorVersion}.{minorVersion}.
*
* @param libraryString On the form {machineName} {majorVersion}.{minorVersion}
* @return Object with keys machineName, majorVersion and minorVersion. Null if string is not parsable.
*/
libraryFromString(libraryString: string): {machineName: string, majorVersion: number, minorVersion: number} {
const matches = libraryString.match(/^([\w0-9\-\.]{1,255})[\-\ ]([0-9]{1,5})\.([0-9]{1,5})$/i);
if (matches && matches.length >= 4) {
return {
machineName: matches[1],
majorVersion: Number(matches[2]),
minorVersion: Number(matches[3])
};
}
return null;
}
/**
* Convert list of library parameter values to csv.
*
* @param libraryData Library data as found in library.json files.
* @param key Key that should be found in libraryData.
* @param searchParam The library parameter (Default: 'path').
* @return Library parameter values separated by ', '
*/
libraryParameterValuesToCsv(libraryData: any, key: string, searchParam: string = 'path'): string {
if (typeof libraryData[key] != 'undefined') {
const parameterValues = [];
libraryData[key].forEach((file) => {
for (const index in file) {
if (index === searchParam) {
parameterValues.push(file[index]);
}
}
});
return parameterValues.join(',');
}
return '';
}
/**
* Convert strings of text into simple kebab case slugs. Based on H5PCore::slugify.
*
* @param input The string to slugify.
* @return Slugified text.
*/
slugify(input: string): string {
input = input || '';
input = input.toLowerCase();
// Replace common chars.
let newInput = '';
for (let i = 0; i < input.length; i++) {
const char = input[i];
newInput += this.SLUGIFY_MAP[char] || char;
}
// Replace everything else.
newInput = newInput.replace(/[^a-z0-9]/g, '-');
// Prevent double hyphen
newInput = newInput.replace(/-{2,}/g, '-');
// Prevent hyphen in beginning or end.
newInput = newInput.replace(/(^-+|-+$)/g, '');
// Prevent too long slug.
if (newInput.length > 91) {
newInput = newInput.substr(0, 92);
}
// Prevent empty slug
if (newInput === '') {
newInput = 'interactive';
}
return newInput;
}
/**
* Determine if params contain any match.
*
* @param params Parameters.
* @param pattern Regular expression to identify pattern.
* @return True if params matches pattern.
*/
textAddonMatches(params: any, pattern: string): boolean {
if (typeof params == 'string') {
if (params.match(pattern)) {
return true;
}
} else if (typeof params == 'object') {
for (const key in params) {
const value = params[key];
if (this.textAddonMatches(value, pattern)) {
return true;
}
}
}
return false;
}
}

View File

@ -516,7 +516,7 @@ export class CoreQuestionHelperProvider {
*/
prefetchQuestionFiles(question: any, component?: string, componentId?: string | number, siteId?: string, usageId?: number)
: Promise<any> {
const urls = this.domUtils.extractDownloadableFilesFromHtml(question.html);
const urls = this.filepoolProvider.extractDownloadableFilesFromHtml(question.html);
if (!component) {
component = CoreQuestionProvider.COMPONENT;

View File

@ -22,6 +22,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreSitePluginsProvider } from '../../providers/siteplugins';
import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/**
* Handler to prefetch a module site plugin.
@ -39,13 +40,15 @@ export class CoreSitePluginsModulePrefetchHandler extends CoreCourseActivityPref
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected sitePluginsProvider: CoreSitePluginsProvider,
component: string,
name: string,
modName: string,
protected handlerSchema: any) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper);
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper,
pluginFileDelegate);
this.component = component;
this.name = name;

View File

@ -32,6 +32,7 @@ import { CoreQuestionProvider } from '@core/question/providers/question';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
// Delegates
import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
@ -117,7 +118,8 @@ export class CoreSitePluginsHelperProvider {
private workshopAssessmentStrategyDelegate: AddonWorkshopAssessmentStrategyDelegate,
private courseProvider: CoreCourseProvider,
private blockDelegate: CoreBlockDelegate,
private filterHelper: CoreFilterHelperProvider) {
private filterHelper: CoreFilterHelperProvider,
private pluginFileDelegate: CorePluginFileDelegate) {
this.logger = loggerProvider.getInstance('CoreSitePluginsHelperProvider');
@ -841,7 +843,7 @@ export class CoreSitePluginsHelperProvider {
// Register the prefetch handler.
this.prefetchDelegate.registerHandler(new CoreSitePluginsModulePrefetchHandler(this.translate, this.appProvider,
this.utils, this.courseProvider, this.filepoolProvider, this.sitesProvider, this.domUtils, this.filterHelper,
this.sitePluginsProvider, plugin.component, uniqueName, modName, handlerSchema));
this.pluginFileDelegate, this.sitePluginsProvider, plugin.component, uniqueName, modName, handlerSchema));
}
return uniqueName;

View File

@ -12,7 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core';
import {
Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional, ViewContainerRef
} from '@angular/core';
import { Platform, NavController, Content } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
@ -90,7 +92,8 @@ export class CoreFormatTextDirective implements OnChanges {
private eventsProvider: CoreEventsProvider,
private filterProvider: CoreFilterProvider,
private filterHelper: CoreFilterHelperProvider,
private filterDelegate: CoreFilterDelegate) {
private filterDelegate: CoreFilterDelegate,
private viewContainerRef: ViewContainerRef) {
this.element = element.nativeElement;
this.element.classList.add('opacity-hide'); // Hide contents until they're treated.
@ -371,7 +374,8 @@ export class CoreFormatTextDirective implements OnChanges {
if (result.options.filter) {
// Let filters hnadle HTML. We do it here because we don't want them to block the render of the text.
this.filterDelegate.handleHtml(this.element, result.filters, result.options, [], result.siteId);
this.filterDelegate.handleHtml(this.element, result.filters, this.viewContainerRef, result.options, [],
this.component, this.componentId, result.siteId);
}
this.element.classList.remove('core-disable-media-adapt');
@ -400,7 +404,10 @@ export class CoreFormatTextDirective implements OnChanges {
// Error getting the site. This probably means that there is no current site and no siteId was supplied.
}).then((siteInstance: CoreSite) => {
site = siteInstance;
result.siteId = site.getId();
if (site) {
result.siteId = site.getId();
}
if (this.contextLevel == 'course' && this.contextInstanceId <= 0) {
this.contextInstanceId = site.getSiteHomeId();
@ -418,14 +425,14 @@ export class CoreFormatTextDirective implements OnChanges {
if (this.filter) {
return this.filterHelper.getFiltersAndFormatText(this.text, this.contextLevel, this.contextInstanceId,
result.options, site.getId()).then((res) => {
result.options, result.siteId).then((res) => {
result.filters = res.filters;
return res.text;
});
} else {
return this.filterProvider.formatText(this.text, result.options, [], site.getId());
return this.filterProvider.formatText(this.text, result.options, [], result.siteId);
}
}).then((formatted) => {

View File

@ -52,6 +52,7 @@ export class CoreFileProvider {
static FORMATDATAURL = 1;
static FORMATBINARYSTRING = 2;
static FORMATARRAYBUFFER = 3;
static FORMATJSON = 4;
// Folders.
static SITESFOLDER = 'sites';
@ -491,6 +492,7 @@ export class CoreFileProvider {
* FORMATDATAURL
* FORMATBINARYSTRING
* FORMATARRAYBUFFER
* FORMATJSON
* @return Promise to be resolved when the file is read.
*/
readFile(path: string, format: number = CoreFileProvider.FORMATTEXT): Promise<any> {
@ -505,6 +507,16 @@ export class CoreFileProvider {
return this.file.readAsBinaryString(this.basePath, path);
case CoreFileProvider.FORMATARRAYBUFFER:
return this.file.readAsArrayBuffer(this.basePath, path);
case CoreFileProvider.FORMATJSON:
return this.file.readAsText(this.basePath, path).then((text) => {
const parsed = this.textUtils.parseJSON(text, null);
if (parsed == null && text != null) {
return Promise.reject('Error parsing JSON file: ' + path);
}
return parsed;
});
default:
return this.file.readAsText(this.basePath, path);
}
@ -519,6 +531,7 @@ export class CoreFileProvider {
* FORMATDATAURL
* FORMATBINARYSTRING
* FORMATARRAYBUFFER
* FORMATJSON
* @return Promise to be resolved when the file is read.
*/
readFileData(fileData: any, format: number = CoreFileProvider.FORMATTEXT): Promise<any> {
@ -531,7 +544,18 @@ export class CoreFileProvider {
reader.onloadend = (evt): void => {
const target = <any> evt.target; // Convert to <any> to be able to use non-standard properties.
if (target.result !== undefined || target.result !== null) {
resolve(target.result);
if (format == CoreFileProvider.FORMATJSON) {
// Convert to object.
const parsed = this.textUtils.parseJSON(target.result, null);
if (parsed == null) {
reject('Error parsing JSON file.');
}
resolve(parsed);
} else {
resolve(target.result);
}
} else if (target.error !== undefined || target.error !== null) {
reject(target.error);
} else {
@ -728,19 +752,58 @@ export class CoreFileProvider {
}
}
/**
* Move a dir.
*
* @param originalPath Path to the dir to move.
* @param newPath New path of the dir.
* @param destDirExists Set it to true if you know the directory where to put the dir exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is moved.
*/
moveDir(originalPath: string, newPath: string, destDirExists?: boolean): Promise<any> {
return this.moveFileOrDir(originalPath, newPath, true, destDirExists);
}
/**
* Move a file.
*
* @param originalPath Path to the file to move.
* @param newPath New path of the file.
* @param destDirExists Set it to true if you know the directory where to put the file exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is moved.
*/
moveFile(originalPath: string, newPath: string): Promise<any> {
moveFile(originalPath: string, newPath: string, destDirExists?: boolean): Promise<any> {
return this.moveFileOrDir(originalPath, newPath, false, destDirExists);
}
/**
* Move a file/dir.
*
* @param originalPath Path to the file/dir to move.
* @param newPath New path of the file/dir.
* @param isDir Whether it's a dir or a file.
* @param destDirExists Set it to true if you know the directory where to put the file/dir exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is moved.
*/
protected moveFileOrDir(originalPath: string, newPath: string, isDir?: boolean, destDirExists?: boolean): Promise<any> {
const moveFn = isDir ? this.file.moveDir.bind(this.file) : this.file.moveFile.bind(this.file);
return this.init().then(() => {
// Remove basePath if it's in the paths.
originalPath = this.removeStartingSlash(originalPath.replace(this.basePath, ''));
newPath = this.removeStartingSlash(newPath.replace(this.basePath, ''));
const newPathFileAndDir = this.getFileAndDirectoryFromPath(newPath);
if (newPathFileAndDir.directory && !destDirExists) {
// Create the target directory if it doesn't exist.
return this.createDir(newPathFileAndDir.directory);
}
}).then(() => {
if (this.isHTMLAPI) {
// In Cordova API we need to calculate the longest matching path to make it work.
// The function this.file.moveFile('a/', 'b/c.ext', 'a/', 'b/d.ext') doesn't work.
@ -763,15 +826,15 @@ export class CoreFileProvider {
}
}
return this.file.moveFile(commonPath, originalPath, commonPath, newPath);
return moveFn(commonPath, originalPath, commonPath, newPath);
} else {
return this.file.moveFile(this.basePath, originalPath, this.basePath, newPath).catch((error) => {
return moveFn(this.basePath, originalPath, this.basePath, newPath).catch((error) => {
// The move can fail if the path has encoded characters. Try again if that's the case.
const decodedOriginal = decodeURI(originalPath),
decodedNew = decodeURI(newPath);
if (decodedOriginal != originalPath || decodedNew != newPath) {
return this.file.moveFile(this.basePath, decodedOriginal, this.basePath, decodedNew);
return moveFn(this.basePath, decodedOriginal, this.basePath, decodedNew);
} else {
return Promise.reject(error);
}
@ -780,16 +843,46 @@ export class CoreFileProvider {
});
}
/**
* Copy a directory.
*
* @param from Path to the directory to move.
* @param to New path of the directory.
* @param destDirExists Set it to true if you know the directory where to put the dir exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is copied.
*/
copyDir(from: string, to: string, destDirExists?: boolean): Promise<any> {
return this.copyFileOrDir(from, to, true, destDirExists);
}
/**
* Copy a file.
*
* @param from Path to the file to move.
* @param to New path of the file.
* @param destDirExists Set it to true if you know the directory where to put the file exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is copied.
*/
copyFile(from: string, to: string): Promise<any> {
copyFile(from: string, to: string, destDirExists?: boolean): Promise<any> {
return this.copyFileOrDir(from, to, false, destDirExists);
}
/**
* Copy a file or a directory.
*
* @param from Path to the file/dir to move.
* @param to New path of the file/dir.
* @param isDir Whether it's a dir or a file.
* @param destDirExists Set it to true if you know the directory where to put the file/dir exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is copied.
*/
protected copyFileOrDir(from: string, to: string, isDir?: boolean, destDirExists?: boolean): Promise<any> {
let fromFileAndDir,
toFileAndDir;
const copyFn = isDir ? this.file.copyDir.bind(this.file) : this.file.copyFile.bind(this.file);
return this.init().then(() => {
// Paths cannot start with "/". Remove basePath if present.
@ -799,7 +892,7 @@ export class CoreFileProvider {
fromFileAndDir = this.getFileAndDirectoryFromPath(from);
toFileAndDir = this.getFileAndDirectoryFromPath(to);
if (toFileAndDir.directory) {
if (toFileAndDir.directory && !destDirExists) {
// Create the target directory if it doesn't exist.
return this.createDir(toFileAndDir.directory);
}
@ -809,15 +902,15 @@ export class CoreFileProvider {
const fromDir = this.textUtils.concatenatePaths(this.basePath, fromFileAndDir.directory),
toDir = this.textUtils.concatenatePaths(this.basePath, toFileAndDir.directory);
return this.file.copyFile(fromDir, fromFileAndDir.name, toDir, toFileAndDir.name);
return copyFn(fromDir, fromFileAndDir.name, toDir, toFileAndDir.name);
} else {
return this.file.copyFile(this.basePath, from, this.basePath, to).catch((error) => {
return copyFn(this.basePath, from, this.basePath, to).catch((error) => {
// The copy can fail if the path has encoded characters. Try again if that's the case.
const decodedFrom = decodeURI(from),
decodedTo = decodeURI(to);
if (from != decodedFrom || to != decodedTo) {
return this.file.copyFile(this.basePath, decodedFrom, this.basePath, decodedTo);
return copyFn(this.basePath, decodedFrom, this.basePath, decodedTo);
} else {
return Promise.reject(error);
}
@ -898,11 +991,26 @@ export class CoreFileProvider {
* @param destFolder Path to the destination folder. If not defined, a new folder will be created with the
* same location and name as the ZIP file (without extension).
* @param onProgress Function to call on progress.
* @param recreateDir Delete the dest directory before unzipping. Defaults to true.
* @return Promise resolved when the file is unzipped.
*/
unzipFile(path: string, destFolder?: string, onProgress?: Function): Promise<any> {
unzipFile(path: string, destFolder?: string, onProgress?: Function, recreateDir: boolean = true): Promise<any> {
// Get the source file.
return this.getFile(path).then((fileEntry) => {
let fileEntry: FileEntry;
return this.getFile(path).then((fe) => {
fileEntry = fe;
if (destFolder && recreateDir) {
// Make sure the dest dir doesn't exist already.
return this.removeDir(destFolder).catch(() => {
// Ignore errors.
}).then(() => {
// Now create the dir, otherwise if any of the ancestor dirs doesn't exist the unzip would fail.
return this.createDir(destFolder);
});
}
}).then(() => {
// If destFolder is not set, use same location as ZIP file. We need to use absolute paths (including basePath).
destFolder = this.addBasePathIfNeeded(destFolder || this.mimeUtils.removeExtension(path));
@ -1146,4 +1254,19 @@ export class CoreFileProvider {
isFileInAppFolder(path: string): boolean {
return path.indexOf(this.basePath) != -1;
}
/**
* Get the full path to the www folder at runtime.
*
* @return Path.
*/
getWWWPath(): string {
const position = window.location.href.indexOf('index.html');
if (position != -1) {
return window.location.href.substr(0, position);
}
return window.location.href;
}
}

View File

@ -21,7 +21,7 @@ import { CoreInitDelegate } from './init';
import { CoreLoggerProvider } from './logger';
import { CorePluginFileDelegate } from './plugin-file-delegate';
import { CoreSitesProvider, CoreSiteSchema } from './sites';
import { CoreWSProvider } from './ws';
import { CoreWSProvider, CoreWSExternalFile } from './ws';
import { CoreDomUtilsProvider } from './utils/dom';
import { CoreMimetypeUtilsProvider } from './utils/mimetype';
import { CoreTextUtilsProvider } from './utils/text';
@ -473,8 +473,8 @@ export class CoreFilepoolProvider {
* downloading a file automatically does this. Note that this method does not check if the file exists in the pool.
*/
addFileLinkByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number): Promise<any> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => {
const fileId = this.getFileIdByUrl(fileUrl);
return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(file.fileurl);
return this.addFileLink(siteId, fileId, component, componentId);
});
@ -605,11 +605,12 @@ export class CoreFilepoolProvider {
* @param priority The priority this file should get in the queue (range 0-999).
* @param options Extra options (isexternalfile, repositorytype).
* @param revision File revision. If not defined, it will be calculated using the URL.
* @param alreadyFixed Whether the URL has already been fixed.
* @return Resolved on success.
*/
addToQueueByUrl(siteId: string, fileUrl: string, component?: string, componentId?: string | number, timemodified: number = 0,
filePath?: string, onProgress?: (event: any) => any, priority: number = 0, options: any = {}, revision?: number)
: Promise<any> {
filePath?: string, onProgress?: (event: any) => any, priority: number = 0, options: any = {}, revision?: number,
alreadyFixed?: boolean): Promise<any> {
let fileId,
link,
queueDeferred;
@ -623,94 +624,102 @@ export class CoreFilepoolProvider {
return Promise.reject(null);
}
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => {
const primaryKey = { siteId: siteId, fileId: fileId };
if (alreadyFixed) {
// Already fixed, if we reached here it means it can be downloaded.
return <CoreWSExternalFile> {fileurl: fileUrl};
} else {
return this.fixPluginfileURL(siteId, fileUrl);
}
}).then((file) => {
revision = revision || this.getRevisionFromUrl(fileUrl);
fileId = this.getFileIdByUrl(fileUrl);
fileUrl = file.fileurl;
timemodified = file.timemodified || timemodified;
revision = revision || this.getRevisionFromUrl(fileUrl);
fileId = this.getFileIdByUrl(fileUrl);
// Set up the component.
if (typeof component != 'undefined') {
link = {
component: component,
componentId: this.fixComponentId(componentId)
};
}
const primaryKey = { siteId: siteId, fileId: fileId };
// Retrieve the queue deferred now if it exists.
// This is to prevent errors if file is removed from queue while we're checking if the file is in queue.
queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress);
// Set up the component.
if (typeof component != 'undefined') {
link = {
component: component,
componentId: this.fixComponentId(componentId)
};
}
return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => {
const newData: any = {};
let foundLink = false;
// Retrieve the queue deferred now if it exists.
// This is to prevent errors if file is removed from queue while we're checking if the file is in queue.
queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress);
if (entry) {
// We already have the file in queue, we update the priority and links.
if (entry.priority < priority) {
newData.priority = priority;
}
if (revision && entry.revision !== revision) {
newData.revision = revision;
}
if (timemodified && entry.timemodified !== timemodified) {
newData.timemodified = timemodified;
}
if (filePath && entry.path !== filePath) {
newData.path = filePath;
}
if (entry.isexternalfile !== options.isexternalfile && (entry.isexternalfile || options.isexternalfile)) {
newData.isexternalfile = options.isexternalfile;
}
if (entry.repositorytype !== options.repositorytype && (entry.repositorytype || options.repositorytype)) {
newData.repositorytype = options.repositorytype;
}
return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => {
const newData: any = {};
let foundLink = false;
if (link) {
// We need to add the new link if it does not exist yet.
if (entry.links && entry.links.length) {
for (const i in entry.links) {
const fileLink = entry.links[i];
if (fileLink.component == link.component && fileLink.componentId == link.componentId) {
foundLink = true;
break;
}
if (entry) {
// We already have the file in queue, we update the priority and links.
if (entry.priority < priority) {
newData.priority = priority;
}
if (revision && entry.revision !== revision) {
newData.revision = revision;
}
if (timemodified && entry.timemodified !== timemodified) {
newData.timemodified = timemodified;
}
if (filePath && entry.path !== filePath) {
newData.path = filePath;
}
if (entry.isexternalfile !== options.isexternalfile && (entry.isexternalfile || options.isexternalfile)) {
newData.isexternalfile = options.isexternalfile;
}
if (entry.repositorytype !== options.repositorytype && (entry.repositorytype || options.repositorytype)) {
newData.repositorytype = options.repositorytype;
}
if (link) {
// We need to add the new link if it does not exist yet.
if (entry.links && entry.links.length) {
for (const i in entry.links) {
const fileLink = entry.links[i];
if (fileLink.component == link.component && fileLink.componentId == link.componentId) {
foundLink = true;
break;
}
}
if (!foundLink) {
newData.links = entry.links || [];
newData.links.push(link);
newData.links = JSON.stringify(entry.links);
}
}
if (Object.keys(newData).length) {
// Update only when required.
this.logger.debug(`Updating file ${fileId} which is already in queue`);
return this.appDB.updateRecords(this.QUEUE_TABLE, newData, primaryKey).then(() => {
return this.getQueuePromise(siteId, fileId, true, onProgress);
});
if (!foundLink) {
newData.links = entry.links || [];
newData.links.push(link);
newData.links = JSON.stringify(entry.links);
}
this.logger.debug(`File ${fileId} already in queue and does not require update`);
if (queueDeferred) {
// If we were able to retrieve the queue deferred before, we use that one.
return queueDeferred.promise;
} else {
// Create a new deferred and return its promise.
return this.getQueuePromise(siteId, fileId, true, onProgress);
}
} else {
return this.addToQueue(
siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link);
}
}, () => {
// Unsure why we could not get the record, let's add to the queue anyway.
if (Object.keys(newData).length) {
// Update only when required.
this.logger.debug(`Updating file ${fileId} which is already in queue`);
return this.appDB.updateRecords(this.QUEUE_TABLE, newData, primaryKey).then(() => {
return this.getQueuePromise(siteId, fileId, true, onProgress);
});
}
this.logger.debug(`File ${fileId} already in queue and does not require update`);
if (queueDeferred) {
// If we were able to retrieve the queue deferred before, we use that one.
return queueDeferred.promise;
} else {
// Create a new deferred and return its promise.
return this.getQueuePromise(siteId, fileId, true, onProgress);
}
} else {
return this.addToQueue(
siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link);
});
}
}, () => {
// Unsure why we could not get the record, let's add to the queue anyway.
return this.addToQueue(
siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link);
});
});
}
@ -719,7 +728,7 @@ export class CoreFilepoolProvider {
* Adds a file to the queue if the size is allowed to be downloaded.
*
* @param siteId The site ID.
* @param fileUrl The absolute URL to the file.
* @param fileUrl The absolute URL to the file, already fixed.
* @param component The component to link the file to.
* @param componentId An ID to use in conjunction with the component.
* @param timemodified The time this file was modified.
@ -760,18 +769,18 @@ export class CoreFilepoolProvider {
// Check if the file should be downloaded.
if (sizeUnknown) {
if (downloadUnknown && isWifi) {
return this.addToQueueByUrl(
siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, revision);
return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined,
0, options, revision, true);
}
} else if (size <= this.DOWNLOAD_THRESHOLD || (isWifi && size <= this.WIFI_DOWNLOAD_THRESHOLD)) {
return this.addToQueueByUrl(
siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, revision);
} else if (this.shouldDownload(size)) {
return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0,
options, revision, true);
}
});
} else {
// No need to check size, just add it to the queue.
return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options,
revision);
revision, true);
}
}
@ -938,7 +947,13 @@ export class CoreFilepoolProvider {
return Promise.reject(null);
}
return this.wsProvider.downloadFile(fileUrl, filePath, addExtension, onProgress).then((fileEntry) => {
let fileEntry;
return this.wsProvider.downloadFile(fileUrl, filePath, addExtension, onProgress).then((entry) => {
fileEntry = entry;
return this.pluginFileDelegate.treatDownloadedFile(fileUrl, fileEntry, siteId);
}).then(() => {
const data: CoreFilepoolFileEntry = poolFileObject || {};
data.downloadTime = Date.now();
@ -1157,8 +1172,10 @@ export class CoreFilepoolProvider {
promise;
if (this.fileProvider.isAvailable()) {
return this.fixPluginfileURL(siteId, fileUrl).then((fixedUrl) => {
fileUrl = fixedUrl;
return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
fileUrl = file.fileurl;
timemodified = file.timemodified || timemodified;
options = Object.assign({}, options); // Create a copy to prevent modifying the original object.
options.timemodified = timemodified || 0;
@ -1222,6 +1239,59 @@ export class CoreFilepoolProvider {
}
}
/**
* Extract the downloadable URLs from an HTML code.
*
* @param html HTML code.
* @return List of file urls.
*/
extractDownloadableFilesFromHtml(html: string): string[] {
let urls = [],
elements;
const element = this.domUtils.convertToElement(html);
elements = element.querySelectorAll('a, img, audio, video, source, track');
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
let url = element.tagName === 'A' ? element.href : element.src;
if (url && this.urlUtils.isDownloadableUrl(url) && urls.indexOf(url) == -1) {
urls.push(url);
}
// Treat video poster.
if (element.tagName == 'VIDEO' && element.getAttribute('poster')) {
url = element.getAttribute('poster');
if (url && this.urlUtils.isDownloadableUrl(url) && urls.indexOf(url) == -1) {
urls.push(url);
}
}
}
// Now get other files from plugin file handlers.
urls = urls.concat(this.pluginFileDelegate.getDownloadableFilesFromHTML(element));
return urls;
}
/**
* Extract the downloadable URLs from an HTML code and returns them in fake file objects.
*
* @param html HTML code.
* @return List of fake file objects with file URLs.
*/
extractDownloadableFilesFromHtmlAsFakeFileObjects(html: string): any[] {
const urls = this.extractDownloadableFilesFromHtml(html);
// Convert them to fake file objects.
return urls.map((url) => {
return {
fileurl: url
};
});
}
/**
* Fill Missing Extension In the File Object if needed.
* This is to migrate from old versions.
@ -1313,15 +1383,24 @@ export class CoreFilepoolProvider {
}
/**
* Add the wstoken url and points to the correct script.
* Check whether the file can be downloaded, add the wstoken url and points to the correct script.
*
* @param siteId The site ID.
* @param fileUrl The file URL.
* @return Resolved with fixed URL on success, rejected otherwise.
* @param timemodified The timemodified of the file.
* @return Promise resolved with the file data to use.
*/
protected fixPluginfileURL(siteId: string, fileUrl: string): Promise<string> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.checkAndFixPluginfileURL(fileUrl);
protected fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise<CoreWSExternalFile> {
return this.pluginFileDelegate.getDownloadableFile({fileurl: fileUrl, timemodified: timemodified}).then((file) => {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.checkAndFixPluginfileURL(file.fileurl);
}).then((fixedUrl) => {
file.fileurl = fixedUrl;
return file;
});
});
}
@ -1351,8 +1430,8 @@ export class CoreFilepoolProvider {
*/
getDirectoryUrlByUrl(siteId: string, fileUrl: string): Promise<string> {
if (this.fileProvider.isAvailable()) {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => {
const fileId = this.getFileIdByUrl(fileUrl),
return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(file.fileurl),
filePath = <string> this.getFilePath(siteId, fileId, ''); // No extension, the function will return a string.
return this.fileProvider.getDir(filePath).then((dirEntry) => {
@ -1394,8 +1473,8 @@ export class CoreFilepoolProvider {
* @return Promise resolved with event name.
*/
getFileEventNameByUrl(siteId: string, fileUrl: string): Promise<string> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => {
const fileId = this.getFileIdByUrl(fileUrl);
return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(file.fileurl);
return this.getFileEventName(siteId, fileId);
});
@ -1490,8 +1569,8 @@ export class CoreFilepoolProvider {
* @return Promise resolved with the path to the file relative to storage root.
*/
getFilePathByUrl(siteId: string, fileUrl: string): Promise<string> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => {
const fileId = this.getFileIdByUrl(fileUrl);
return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(file.fileurl);
return this.getFilePath(siteId, fileId);
});
@ -1587,8 +1666,10 @@ export class CoreFilepoolProvider {
: Promise<string> {
let fileId;
return this.fixPluginfileURL(siteId, fileUrl).then((fixedUrl) => {
fileUrl = fixedUrl;
return this.fixPluginfileURL(siteId, fileUrl, timemodified).then((file) => {
fileUrl = file.fileurl;
timemodified = file.timemodified || timemodified;
revision = revision || this.getRevisionFromUrl(fileUrl);
fileId = this.getFileIdByUrl(fileUrl);
@ -1618,6 +1699,8 @@ export class CoreFilepoolProvider {
});
});
});
}, () => {
return CoreConstants.NOT_DOWNLOADABLE;
});
}
@ -1655,8 +1738,10 @@ export class CoreFilepoolProvider {
});
};
return this.fixPluginfileURL(siteId, fileUrl).then((fixedUrl) => {
fileUrl = fixedUrl;
return this.fixPluginfileURL(siteId, fileUrl, timemodified).then((file) => {
fileUrl = file.fileurl;
timemodified = file.timemodified || timemodified;
revision = revision || this.getRevisionFromUrl(fileUrl);
fileId = this.getFileIdByUrl(fileUrl);
@ -1779,8 +1864,8 @@ export class CoreFilepoolProvider {
*/
getInternalUrlByUrl(siteId: string, fileUrl: string): Promise<string> {
if (this.fileProvider.isAvailable()) {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => {
const fileId = this.getFileIdByUrl(fileUrl);
return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(file.fileurl);
return this.getInternalUrlById(siteId, fileId);
});
@ -1843,8 +1928,8 @@ export class CoreFilepoolProvider {
* @return Promise resolved with the path of the package.
*/
getPackageDirPathByUrl(siteId: string, url: string): Promise<string> {
return this.fixPluginfileURL(siteId, url).then((fixedUrl) => {
const dirName = this.getPackageDirNameByUrl(fixedUrl);
return this.fixPluginfileURL(siteId, url).then((file) => {
const dirName = this.getPackageDirNameByUrl(file.fileurl);
return this.getFilePath(siteId, dirName, '');
});
@ -1859,8 +1944,8 @@ export class CoreFilepoolProvider {
*/
getPackageDirUrlByUrl(siteId: string, url: string): Promise<string> {
if (this.fileProvider.isAvailable()) {
return this.fixPluginfileURL(siteId, url).then((fixedUrl) => {
const dirName = this.getPackageDirNameByUrl(fixedUrl),
return this.fixPluginfileURL(siteId, url).then((file) => {
const dirName = this.getPackageDirNameByUrl(file.fileurl),
dirPath = <string> this.getFilePath(siteId, dirName, ''); // No extension, the function will return a string.
return this.fileProvider.getDir(dirPath).then((dirEntry) => {
@ -2270,8 +2355,8 @@ export class CoreFilepoolProvider {
* Please note that, if a file is stale, the user will be presented the stale file if there is no network access.
*/
invalidateFileByUrl(siteId: string, fileUrl: string): Promise<any> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => {
const fileId = this.getFileIdByUrl(fileUrl);
return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(file.fileurl);
return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.updateRecords(this.FILES_TABLE, { stale: 1 }, { fileId: fileId });
@ -2318,8 +2403,8 @@ export class CoreFilepoolProvider {
* @param Promise resolved if file is downloading, rejected otherwise.
*/
isFileDownloadingByUrl(siteId: string, fileUrl: string): Promise<any> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => {
const fileId = this.getFileIdByUrl(fileUrl);
return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(file.fileurl);
return this.hasFileInQueue(siteId, fileId);
});
@ -2614,7 +2699,22 @@ export class CoreFilepoolProvider {
protected removeFileById(siteId: string, fileId: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
// Get the path to the file first since it relies on the file object stored in the pool.
return Promise.resolve(this.getFilePath(siteId, fileId)).then((path) => {
// Don't use getFilePath to prevent performing 2 DB requests.
let path = this.getFilepoolFolderPath(siteId) + '/' + fileId,
fileUrl;
return this.hasFileInPool(siteId, fileId).then((entry) => {
fileUrl = entry.url;
if (entry.extension) {
path += '.' + entry.extension;
}
return path;
}).catch(() => {
// If file not found, use the path without extension.
return path;
}).then((path) => {
const promises = [];
// Remove entry from filepool store.
@ -2636,6 +2736,10 @@ export class CoreFilepoolProvider {
return Promise.all(promises).then(() => {
this.notifyFileDeleted(siteId, fileId);
return this.pluginFileDelegate.fileDeleted(fileUrl, path, siteId).catch((error) => {
// Ignore errors.
});
});
});
});
@ -2667,8 +2771,8 @@ export class CoreFilepoolProvider {
* @return Resolved on success, rejected on failure.
*/
removeFileByUrl(siteId: string, fileUrl: string): Promise<any> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => {
const fileId = this.getFileIdByUrl(fileUrl);
return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(file.fileurl);
return this.removeFileById(siteId, fileId);
});
@ -2728,6 +2832,16 @@ export class CoreFilepoolProvider {
});
}
/**
* Check if a file should be downloaded based on its size.
*
* @param size File size.
* @return Whether file should be downloaded.
*/
shouldDownload(size: number): boolean {
return size <= this.DOWNLOAD_THRESHOLD || (this.appProvider.isWifi() && size <= this.WIFI_DOWNLOAD_THRESHOLD);
}
/**
* Convenience function to check if a file should be downloaded before opening it.
*

View File

@ -13,21 +13,23 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreEventsProvider } from './events';
import { CoreLoggerProvider } from './logger';
import { CoreSitesProvider } from './sites';
import { CoreWSExternalFile } from '@providers/ws';
import { FileEntry } from '@ionic-native/file';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
/**
* Interface that all plugin file handlers must implement.
*/
export interface CorePluginFileHandler {
/**
* A name to identify the handler.
*/
name: string;
export interface CorePluginFileHandler extends CoreDelegateHandler {
/**
* The "component" of the handler. It should match the "component" of pluginfile URLs.
* It is used to treat revision from URLs.
*/
component: string;
component?: string;
/**
* Return the RegExp to match the revision on pluginfile URLs.
@ -44,30 +46,125 @@ export interface CorePluginFileHandler {
* @return String to remove the revision on pluginfile url.
*/
getComponentRevisionReplace?(args: string[]): string;
/**
* React to a file being deleted.
*
* @param fileUrl The file URL used to download the file.
* @param path The path of the deleted file.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
fileDeleted?(fileUrl: string, path: string, siteId?: string): Promise<any>;
/**
* Check whether a file can be downloaded. If so, return the file to download.
*
* @param file The file data.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the file to use. Rejected if cannot download.
*/
getDownloadableFile?(file: CoreWSExternalFile, siteId?: string): Promise<CoreWSExternalFile>;
/**
* Given an HTML element, get the URLs of the files that should be downloaded and weren't treated by
* CoreFilepoolProvider.extractDownloadableFilesFromHtml.
*
* @param container Container where to get the URLs from.
* @return {string[]} List of URLs.
*/
getDownloadableFilesFromHTML?(container: HTMLElement): string[];
/**
* Get a file size.
*
* @param file The file data.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the size.
*/
getFileSize?(file: CoreWSExternalFile, siteId?: string): Promise<number>;
/**
* Check whether the file should be treated by this handler. It is used in functions where the component isn't used.
*
* @param file The file data.
* @return Whether the file should be treated by this handler.
*/
shouldHandleFile?(file: CoreWSExternalFile): boolean;
/**
* Treat a downloaded file.
*
* @param fileUrl The file URL used to download the file.
* @param file The file entry of the downloaded file.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
treatDownloadedFile?(fileUrl: string, file: FileEntry, siteId?: string): Promise<any>;
}
/**
* Delegate to register pluginfile information handlers.
*/
@Injectable()
export class CorePluginFileDelegate {
protected logger;
protected handlers: { [s: string]: CorePluginFileHandler } = {};
export class CorePluginFileDelegate extends CoreDelegate {
protected handlerNameProperty = 'component';
constructor(logger: CoreLoggerProvider) {
this.logger = logger.getInstance('CorePluginFileDelegate');
constructor(loggerProvider: CoreLoggerProvider,
sitesProvider: CoreSitesProvider,
eventsProvider: CoreEventsProvider) {
super('CorePluginFileDelegate', loggerProvider, sitesProvider, eventsProvider);
}
/**
* Get the handler for a certain pluginfile url.
* React to a file being deleted.
*
* @param component Component of the plugin.
* @return Handler. Undefined if no handler found for the plugin.
* @param fileUrl The file URL used to download the file.
* @param path The path of the deleted file.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
protected getPluginHandler(component: string): CorePluginFileHandler {
if (typeof this.handlers[component] != 'undefined') {
return this.handlers[component];
fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<any> {
const handler = this.getHandlerForFile({fileurl: fileUrl});
if (handler && handler.fileDeleted) {
return handler.fileDeleted(fileUrl, path, siteId);
}
return Promise.resolve();
}
/**
* Check whether a file can be downloaded. If so, return the file to download.
*
* @param file The file data.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the file to use. Rejected if cannot download.
*/
getDownloadableFile(file: CoreWSExternalFile, siteId?: string): Promise<CoreWSExternalFile> {
const handler = this.getHandlerForFile(file);
return this.getHandlerDownloadableFile(file, handler, siteId);
}
/**
* Check whether a file can be downloaded. If so, return the file to download.
*
* @param file The file data.
* @param handler The handler to use.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the file to use. Rejected if cannot download.
*/
protected getHandlerDownloadableFile(file: CoreWSExternalFile, handler: CorePluginFileHandler, siteId?: string)
: Promise<CoreWSExternalFile> {
if (handler && handler.getDownloadableFile) {
return handler.getDownloadableFile(file, siteId).then((newFile) => {
return newFile || file;
});
}
return Promise.resolve(file);
}
/**
@ -78,7 +175,7 @@ export class CorePluginFileDelegate {
*/
getComponentRevisionRegExp(args: string[]): RegExp {
// Get handler based on component (args[1]).
const handler = this.getPluginHandler(args[1]);
const handler = <CorePluginFileHandler> this.getHandler(args[1], true);
if (handler && handler.getComponentRevisionRegExp) {
return handler.getComponentRevisionRegExp(args);
@ -86,22 +183,96 @@ export class CorePluginFileDelegate {
}
/**
* Register a handler.
* Given an HTML element, get the URLs of the files that should be downloaded and weren't treated by
* CoreFilepoolProvider.extractDownloadableFilesFromHtml.
*
* @param handler The handler to register.
* @return True if registered successfully, false otherwise.
* @param container Container where to get the URLs from.
* @return List of URLs.
*/
registerHandler(handler: CorePluginFileHandler): boolean {
if (typeof this.handlers[handler.component] !== 'undefined') {
this.logger.log(`Handler '${handler.component}' already registered`);
getDownloadableFilesFromHTML(container: HTMLElement): string[] {
let files = [];
return false;
for (const component in this.enabledHandlers) {
const handler = <CorePluginFileHandler> this.enabledHandlers[component];
if (handler && handler.getDownloadableFilesFromHTML) {
files = files.concat(handler.getDownloadableFilesFromHTML(container));
}
}
this.logger.log(`Registered handler '${handler.component}'`);
this.handlers[handler.component] = handler;
return files;
}
return true;
/**
* Sum the filesizes from a list of files checking if the size will be partial or totally calculated.
*
* @param files List of files to sum its filesize.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with file size and a boolean to indicate if it is the total size or only partial.
*/
getFilesSize(files: CoreWSExternalFile[], siteId?: string): Promise<{ size: number, total: boolean }> {
const promises = [],
result = {
size: 0,
total: true
};
files.forEach((file) => {
promises.push(this.getFileSize(file, siteId).then((size) => {
if (typeof size == 'undefined') {
// We don't have the file size, cannot calculate its total size.
result.total = false;
} else {
result.size += size;
}
}));
});
return Promise.all(promises).then(() => {
return result;
});
}
/**
* Get a file size.
*
* @param file The file data.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the size.
*/
getFileSize(file: CoreWSExternalFile, siteId?: string): Promise<number> {
const handler = this.getHandlerForFile(file);
// First of all check if file can be downloaded.
return this.getHandlerDownloadableFile(file, handler, siteId).then((file) => {
if (!file) {
return 0;
}
if (handler && handler.getFileSize) {
return handler.getFileSize(file, siteId).catch(() => {
return file.filesize;
});
}
return Promise.resolve(file.filesize);
});
}
/**
* Get a handler to treat a certain file.
*
* @param file File data.
* @return Handler.
*/
protected getHandlerForFile(file: CoreWSExternalFile): CorePluginFileHandler {
for (const component in this.enabledHandlers) {
const handler = <CorePluginFileHandler> this.enabledHandlers[component];
if (handler && handler.shouldHandleFile && handler.shouldHandleFile(file)) {
return handler;
}
}
}
/**
@ -113,7 +284,7 @@ export class CorePluginFileDelegate {
*/
removeRevisionFromUrl(url: string, args: string[]): string {
// Get handler based on component (args[1]).
const handler = this.getPluginHandler(args[1]);
const handler = <CorePluginFileHandler> this.getHandler(args[1], true);
if (handler && handler.getComponentRevisionRegExp && handler.getComponentRevisionReplace) {
const revisionRegex = handler.getComponentRevisionRegExp(args);
@ -124,4 +295,22 @@ export class CorePluginFileDelegate {
return url;
}
/**
* Treat a downloaded file.
*
* @param fileUrl The file URL used to download the file.
* @param file The file entry of the downloaded file.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise<any> {
const handler = this.getHandlerForFile({fileurl: fileUrl});
if (handler && handler.treatDownloadedFile) {
return handler.treatDownloadedFile(fileUrl, file, siteId);
}
return Promise.resolve();
}
}

View File

@ -22,6 +22,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreTextUtilsProvider } from './text';
import { CoreAppProvider } from '../app';
import { CoreConfigProvider } from '../config';
import { CoreLoggerProvider } from '../logger';
import { CoreUrlUtilsProvider } from './url';
import { CoreFileProvider } from '@providers/file';
import { CoreConstants } from '@core/constants';
@ -61,12 +62,24 @@ export class CoreDomUtilsProvider {
protected lastInstanceId = 0;
protected debugDisplay = false; // Whether to display debug messages. Store it in a variable to make it synchronous.
protected displayedAlerts = {}; // To prevent duplicated alerts.
protected logger;
constructor(private translate: TranslateService, private loadingCtrl: LoadingController, private toastCtrl: ToastController,
private alertCtrl: AlertController, private textUtils: CoreTextUtilsProvider, private appProvider: CoreAppProvider,
private platform: Platform, private configProvider: CoreConfigProvider, private urlUtils: CoreUrlUtilsProvider,
private modalCtrl: ModalController, private sanitizer: DomSanitizer, private popoverCtrl: PopoverController,
private fileProvider: CoreFileProvider) {
constructor(private translate: TranslateService,
private loadingCtrl: LoadingController,
private toastCtrl: ToastController,
private alertCtrl: AlertController,
private textUtils: CoreTextUtilsProvider,
private appProvider: CoreAppProvider,
private platform: Platform,
private configProvider: CoreConfigProvider,
private urlUtils: CoreUrlUtilsProvider,
private modalCtrl: ModalController,
private sanitizer: DomSanitizer,
private popoverCtrl: PopoverController,
private fileProvider: CoreFileProvider,
loggerProvider: CoreLoggerProvider) {
this.logger = loggerProvider.getInstance('CoreDomUtilsProvider');
// Check if debug messages should be displayed.
configProvider.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, false).then((debugDisplay) => {
@ -250,8 +263,12 @@ export class CoreDomUtilsProvider {
*
* @param html HTML code.
* @return List of file urls.
* @deprecated since 3.8. Use CoreFilepoolProvider.extractDownloadableFilesFromHtml instead.
*/
extractDownloadableFilesFromHtml(html: string): string[] {
this.logger.error('The function extractDownloadableFilesFromHtml has been moved to CoreFilepoolProvider.' +
' Please use that function instead of this one.');
const urls = [];
let elements;
@ -283,6 +300,7 @@ export class CoreDomUtilsProvider {
*
* @param html HTML code.
* @return List of fake file objects with file URLs.
* @deprecated since 3.8. Use CoreFilepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects instead.
*/
extractDownloadableFilesFromHtmlAsFakeFileObjects(html: string): any[] {
const urls = this.extractDownloadableFilesFromHtml(html);
@ -372,7 +390,7 @@ export class CoreDomUtilsProvider {
* @return Formatted size. If size is not valid, returns an empty string.
*/
formatPixelsSize(size: any): string {
if (typeof size == 'string' && (size.indexOf('px') > -1 || size.indexOf('%') > -1)) {
if (typeof size == 'string' && (size.indexOf('px') > -1 || size.indexOf('%') > -1 || size == 'auto' || size == 'initial')) {
// It seems to be a valid size.
return size;
}

View File

@ -319,58 +319,86 @@ export class CoreIframeUtilsProvider {
while (el && el.tagName !== 'A') {
el = el.parentElement;
}
if (!el || el.tagName !== 'A') {
return;
}
const link = <HTMLAnchorElement> el;
const scheme = this.urlUtils.getUrlScheme(link.href);
if (!link.href || (scheme && scheme == 'javascript')) {
// Links with no URL and Javascript links are ignored.
const link = <CoreIframeHTMLAnchorElement> el;
if (!link || link.treated) {
return;
}
if (scheme && scheme != 'file' && scheme != 'filesystem') {
// Scheme suggests it's an external resource.
event.preventDefault();
// Add click listener to the link, this way if the iframe has added a listener to the link it will be executed first.
link.treated = true;
link.addEventListener('click', this.linkClicked.bind(this, element, link));
}, {
capture: true // Use capture to fix this listener not called if the element clicked is too deep in the DOM.
});
}
const frameSrc = element.src || element.data,
frameScheme = this.urlUtils.getUrlScheme(frameSrc);
/**
* A link inside a frame was clicked.
*
* @param element Frame element.
* @param link Link clicked.
* @param event Click event.
*/
protected linkClicked(element: HTMLFrameElement | HTMLObjectElement, link: HTMLAnchorElement, event: Event): void {
if (event.defaultPrevented) {
// Event already prevented by some other code.
return;
}
// If the frame is not local, check the target to identify how to treat the link.
if (frameScheme && frameScheme != 'file' && frameScheme != 'filesystem' &&
(!link.target || link.target == '_self')) {
// Load the link inside the frame itself.
if (element.tagName.toLowerCase() == 'object') {
element.setAttribute('data', link.href);
} else {
element.setAttribute('src', link.href);
}
const scheme = this.urlUtils.getUrlScheme(link.href);
if (!link.href || (scheme && scheme == 'javascript')) {
// Links with no URL and Javascript links are ignored.
return;
}
return;
}
if (scheme && scheme != 'file' && scheme != 'filesystem') {
// Scheme suggests it's an external resource.
event.preventDefault();
// The frame is local or the link needs to be opened in a new window. Open in browser.
if (!this.sitesProvider.isLoggedIn()) {
this.utils.openInBrowser(link.href);
} else {
this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(link.href);
}
} else if (link.target == '_parent' || link.target == '_top' || link.target == '_blank') {
// Opening links with _parent, _top or _blank can break the app. We'll open it in InAppBrowser.
event.preventDefault();
this.utils.openFile(link.href).catch((error) => {
this.domUtils.showErrorModal(error);
});
} else if (this.platform.is('ios') && (!link.target || link.target == '_self')) {
// In cordova ios 4.1.0 links inside iframes stopped working. We'll manually treat them.
event.preventDefault();
const frameSrc = (<HTMLFrameElement> element).src || (<HTMLObjectElement> element).data,
frameScheme = this.urlUtils.getUrlScheme(frameSrc);
// If the frame is not local, check the target to identify how to treat the link.
if (frameScheme && frameScheme != 'file' && frameScheme != 'filesystem' &&
(!link.target || link.target == '_self')) {
// Load the link inside the frame itself.
if (element.tagName.toLowerCase() == 'object') {
element.setAttribute('data', link.href);
} else {
element.setAttribute('src', link.href);
}
return;
}
});
// The frame is local or the link needs to be opened in a new window. Open in browser.
if (!this.sitesProvider.isLoggedIn()) {
this.utils.openInBrowser(link.href);
} else {
this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(link.href);
}
} else if (link.target == '_parent' || link.target == '_top' || link.target == '_blank') {
// Opening links with _parent, _top or _blank can break the app. We'll open it in InAppBrowser.
event.preventDefault();
this.utils.openFile(link.href).catch((error) => {
this.domUtils.showErrorModal(error);
});
} else if (this.platform.is('ios') && (!link.target || link.target == '_self')) {
// In cordova ios 4.1.0 links inside iframes stopped working. We'll manually treat them.
event.preventDefault();
if (element.tagName.toLowerCase() == 'object') {
element.setAttribute('data', link.href);
} else {
element.setAttribute('src', link.href);
}
}
}
}
/**
* Subtype of HTMLAnchorElement, with some calculated data.
*/
type CoreIframeHTMLAnchorElement = HTMLAnchorElement & {
treated?: boolean; // Whether the element has been treated already.
};

View File

@ -514,10 +514,9 @@ export class CoreTextUtilsProvider {
return true;
}
const div = document.createElement('div');
div.innerHTML = content;
this.template.innerHTML = content;
return div.textContent === '' && div.querySelector('img, object, hr') === null;
return this.template.textContent === '' && this.template.content.querySelector('img, object, hr') === null;
}
/**

Some files were not shown because too many files have changed in this diff Show More