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/**/*'], src: ['{{ROOT}}/node_modules/mathjax/localization/**/*'],
dest: '{{WWW}}/lib/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.group": "moodle",
"core.groupsseparate": "moodle", "core.groupsseparate": "moodle",
"core.groupsvisible": "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.hasdatatosync": "local_moodlemobileapp",
"core.help": "moodle", "core.help": "moodle",
"core.hide": "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 { AddonFilterAlgebraModule } from './algebra/algebra.module';
import { AddonFilterCensorModule } from './censor/censor.module'; import { AddonFilterCensorModule } from './censor/censor.module';
import { AddonFilterDataModule } from './data/data.module'; import { AddonFilterDataModule } from './data/data.module';
import { AddonFilterDisplayH5PModule } from './displayh5p/displayh5p.module';
import { AddonFilterEmailProtectModule } from './emailprotect/emailprotect.module'; import { AddonFilterEmailProtectModule } from './emailprotect/emailprotect.module';
import { AddonFilterEmoticonModule } from './emoticon/emoticon.module'; import { AddonFilterEmoticonModule } from './emoticon/emoticon.module';
import { AddonFilterGlossaryModule } from './glossary/glossary.module'; import { AddonFilterGlossaryModule } from './glossary/glossary.module';
@ -34,6 +35,7 @@ import { AddonFilterUrlToLinkModule } from './urltolink/urltolink.module';
AddonFilterAlgebraModule, AddonFilterAlgebraModule,
AddonFilterCensorModule, AddonFilterCensorModule,
AddonFilterDataModule, AddonFilterDataModule,
AddonFilterDisplayH5PModule,
AddonFilterEmailProtectModule, AddonFilterEmailProtectModule,
AddonFilterEmoticonModule, AddonFilterEmoticonModule,
AddonFilterGlossaryModule, AddonFilterGlossaryModule,

View File

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

View File

@ -27,6 +27,8 @@ export class AddonFilterMediaPluginHandler extends CoreFilterDefaultHandler {
name = 'AddonFilterMediaPluginHandler'; name = 'AddonFilterMediaPluginHandler';
filterName = 'mediaplugin'; filterName = 'mediaplugin';
protected template = document.createElement('template'); // A template element to convert HTML to element.
constructor(private textUtils: CoreTextUtilsProvider, constructor(private textUtils: CoreTextUtilsProvider,
private urlUtils: CoreUrlUtilsProvider) { private urlUtils: CoreUrlUtilsProvider) {
super(); super();
@ -44,16 +46,15 @@ export class AddonFilterMediaPluginHandler extends CoreFilterDefaultHandler {
filter(text: string, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string) filter(text: string, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string)
: string | Promise<string> { : string | Promise<string> {
const div = document.createElement('div'); this.template.innerHTML = text;
div.innerHTML = text;
const videos = Array.from(div.querySelectorAll('video')); const videos = Array.from(this.template.content.querySelectorAll('video'));
videos.forEach((video) => { videos.forEach((video) => {
this.treatVideoFilters(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 { AddonModAssignFeedbackDelegate } from './feedback-delegate';
import { AddonModAssignSubmissionDelegate } from './submission-delegate'; import { AddonModAssignSubmissionDelegate } from './submission-delegate';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch assigns. * Handler to prefetch assigns.
@ -51,6 +52,7 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected assignProvider: AddonModAssignProvider, protected assignProvider: AddonModAssignProvider,
protected textUtils: CoreTextUtilsProvider, protected textUtils: CoreTextUtilsProvider,
protected feedbackDelegate: AddonModAssignFeedbackDelegate, protected feedbackDelegate: AddonModAssignFeedbackDelegate,
@ -62,7 +64,8 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
protected assignHelper: AddonModAssignHelperProvider, protected assignHelper: AddonModAssignHelperProvider,
protected syncProvider: AddonModAssignSyncProvider) { 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 { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler';
import { AddonModBookProvider } from './book'; import { AddonModBookProvider } from './book';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch books. * Handler to prefetch books.
@ -42,9 +43,11 @@ export class AddonModBookPrefetchHandler extends CoreCourseResourcePrefetchHandl
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected bookProvider: AddonModBookProvider) { 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 { CoreUserProvider } from '@core/user/providers/user';
import { AddonModChatProvider, AddonModChatChat } from './chat'; import { AddonModChatProvider, AddonModChatChat } from './chat';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch chats. * Handler to prefetch chats.
@ -43,11 +44,13 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
private groupsProvider: CoreGroupsProvider, private groupsProvider: CoreGroupsProvider,
private userProvider: CoreUserProvider, private userProvider: CoreUserProvider,
private chatProvider: AddonModChatProvider) { 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 { AddonModChoiceSyncProvider } from './sync';
import { AddonModChoiceProvider } from './choice'; import { AddonModChoiceProvider } from './choice';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch choices. * Handler to prefetch choices.
@ -46,11 +47,13 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected choiceProvider: AddonModChoiceProvider, protected choiceProvider: AddonModChoiceProvider,
protected userProvider: CoreUserProvider, protected userProvider: CoreUserProvider,
protected injector: Injector) { 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 { AddonModDataSyncProvider } from './sync';
import { AddonModDataHelperProvider } from './helper'; import { AddonModDataHelperProvider } from './helper';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch databases. * Handler to prefetch databases.
@ -47,6 +48,7 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected dataProvider: AddonModDataProvider, protected dataProvider: AddonModDataProvider,
protected timeUtils: CoreTimeUtilsProvider, protected timeUtils: CoreTimeUtilsProvider,
protected dataHelper: AddonModDataHelperProvider, protected dataHelper: AddonModDataHelperProvider,
@ -54,7 +56,8 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
protected commentsProvider: CoreCommentsProvider, protected commentsProvider: CoreCommentsProvider,
protected syncProvider: AddonModDataSyncProvider) { 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 { CoreGroupsProvider } from '@providers/groups';
import { AddonModFeedbackSyncProvider } from './sync'; import { AddonModFeedbackSyncProvider } from './sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch feedbacks. * Handler to prefetch feedbacks.
@ -48,13 +49,15 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected feedbackProvider: AddonModFeedbackProvider, protected feedbackProvider: AddonModFeedbackProvider,
protected feedbackHelper: AddonModFeedbackHelperProvider, protected feedbackHelper: AddonModFeedbackHelperProvider,
protected timeUtils: CoreTimeUtilsProvider, protected timeUtils: CoreTimeUtilsProvider,
protected groupsProvider: CoreGroupsProvider, protected groupsProvider: CoreGroupsProvider,
protected injector: Injector) { 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 // Component + Filearea + Revision
return '/mod_folder/content/0/'; 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 { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler';
import { AddonModFolderProvider } from './folder'; import { AddonModFolderProvider } from './folder';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch folders. * Handler to prefetch folders.
@ -41,9 +42,11 @@ export class AddonModFolderPrefetchHandler extends CoreCourseResourcePrefetchHan
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected folderProvider: AddonModFolderProvider) { 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 { AddonModForumProvider } from './forum';
import { AddonModForumSyncProvider } from './sync'; import { AddonModForumSyncProvider } from './sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch forums. * Handler to prefetch forums.
@ -45,12 +46,14 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
private userProvider: CoreUserProvider, private userProvider: CoreUserProvider,
private groupsProvider: CoreGroupsProvider, private groupsProvider: CoreGroupsProvider,
private forumProvider: AddonModForumProvider, private forumProvider: AddonModForumProvider,
private syncProvider: AddonModForumSyncProvider) { 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) { if (getInlineFiles && post.messageinlinefiles && post.messageinlinefiles.length) {
files = files.concat(post.messageinlinefiles); files = files.concat(post.messageinlinefiles);
} else if (post.message && !getInlineFiles) { } 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 { AddonModGlossaryProvider } from './glossary';
import { AddonModGlossarySyncProvider } from './sync'; import { AddonModGlossarySyncProvider } from './sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch forums. * Handler to prefetch forums.
@ -44,11 +45,13 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected glossaryProvider: AddonModGlossaryProvider, protected glossaryProvider: AddonModGlossaryProvider,
protected commentsProvider: CoreCommentsProvider, protected commentsProvider: CoreCommentsProvider,
protected syncProvider: AddonModGlossarySyncProvider) { 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) { if (getInlineFiles && entry.definitioninlinefiles && entry.definitioninlinefiles.length) {
files = files.concat(entry.definitioninlinefiles); files = files.concat(entry.definitioninlinefiles);
} else if (entry.definition && !getInlineFiles) { } 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 // Component + Filearea + Revision
return '/mod_imscp/' + args[2] + '/0/'; 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 { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler';
import { AddonModImscpProvider } from './imscp'; import { AddonModImscpProvider } from './imscp';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch IMSCPs. * Handler to prefetch IMSCPs.
@ -41,9 +42,11 @@ export class AddonModImscpPrefetchHandler extends CoreCourseResourcePrefetchHand
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected imscpProvider: AddonModImscpProvider) { 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 { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler';
import { AddonModLabelProvider } from './label'; import { AddonModLabelProvider } from './label';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch labels. * Handler to prefetch labels.
@ -43,9 +44,11 @@ export class AddonModLabelPrefetchHandler extends CoreCourseResourcePrefetchHand
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected labelProvider: AddonModLabelProvider) { 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 { AddonModLessonProvider } from './lesson';
import { AddonModLessonSyncProvider } from './lesson-sync'; import { AddonModLessonSyncProvider } from './lesson-sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch lessons. * Handler to prefetch lessons.
@ -48,12 +49,14 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected modalCtrl: ModalController, protected modalCtrl: ModalController,
protected groupsProvider: CoreGroupsProvider, protected groupsProvider: CoreGroupsProvider,
protected lessonProvider: AddonModLessonProvider, protected lessonProvider: AddonModLessonProvider,
protected injector: Injector) { 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 || []; let files = lesson.mediafiles || [];
files = files.concat(this.getIntroFilesFromInstance(module, lesson)); 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. // Get the pages to calculate the size.
return this.lessonProvider.getPages(lesson.id, password, false, false, siteId); return this.lessonProvider.getPages(lesson.id, password, false, false, siteId);
@ -414,7 +419,8 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan
return; return;
} }
answerPage.answerdata.answers.forEach((answer) => { 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 { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler';
import { AddonModLtiProvider } from './lti'; import { AddonModLtiProvider } from './lti';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; 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. * 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, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected ltiProvider: AddonModLtiProvider) { 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 // Component + Filearea + Revision
return '/mod_page/content/0/'; 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 { AddonModPageProvider } from './page';
import { AddonModPageHelperProvider } from './helper'; import { AddonModPageHelperProvider } from './helper';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch pages. * Handler to prefetch pages.
@ -43,10 +44,12 @@ export class AddonModPagePrefetchHandler extends CoreCourseResourcePrefetchHandl
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected pageProvider: AddonModPageProvider, protected pageProvider: AddonModPageProvider,
protected pageHelper: AddonModPageHelperProvider) { 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 { AddonModQuizSyncProvider } from './quiz-sync';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch quizzes. * Handler to prefetch quizzes.
@ -50,6 +51,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected injector: Injector, protected injector: Injector,
protected quizProvider: AddonModQuizProvider, protected quizProvider: AddonModQuizProvider,
protected textUtils: CoreTextUtilsProvider, protected textUtils: CoreTextUtilsProvider,
@ -57,7 +59,8 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl
protected accessRuleDelegate: AddonModQuizAccessRuleDelegate, protected accessRuleDelegate: AddonModQuizAccessRuleDelegate,
protected questionHelper: CoreQuestionHelperProvider) { 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); files = files.concat(feedback.feedbackinlinefiles);
} else if (feedback.feedbacktext && !getInlineFiles) { } else if (feedback.feedbacktext && !getInlineFiles) {
files = files.concat( 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 // Component + Filearea + Revision
return '/mod_resource/content/0/'; 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 { AddonModResourceHelperProvider } from './helper';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch resources. * Handler to prefetch resources.
@ -43,10 +44,12 @@ export class AddonModResourcePrefetchHandler extends CoreCourseResourcePrefetchH
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected resourceProvider: AddonModResourceProvider, protected resourceProvider: AddonModResourceProvider,
protected resourceHelper: AddonModResourceHelperProvider) { 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 // Component + Filearea + Revision
return '/mod_scorm/content/0/'; 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 { AddonModScormProvider } from './scorm';
import { AddonModScormSyncProvider } from './scorm-sync'; import { AddonModScormSyncProvider } from './scorm-sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Progress event used when downloading a SCORM. * Progress event used when downloading a SCORM.
@ -67,12 +68,14 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected fileProvider: CoreFileProvider, protected fileProvider: CoreFileProvider,
protected textUtils: CoreTextUtilsProvider, protected textUtils: CoreTextUtilsProvider,
protected scormProvider: AddonModScormProvider, protected scormProvider: AddonModScormProvider,
protected injector: Injector) { 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, return this.filepoolProvider.downloadUrl(siteId, packageUrl, true, this.component, scorm.coursemodule,
undefined, this.downloadProgress.bind(this, true, onProgress)); 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(() => { }).then(() => {
// Get the ZIP file path. // Get the ZIP file path.
return this.filepoolProvider.getFilePathByUrl(siteId, packageUrl); return this.filepoolProvider.getFilePathByUrl(siteId, packageUrl);

View File

@ -25,6 +25,7 @@ import { AddonModSurveyProvider } from './survey';
import { AddonModSurveySyncProvider } from './sync'; import { AddonModSurveySyncProvider } from './sync';
import { AddonModSurveyHelperProvider } from './helper'; import { AddonModSurveyHelperProvider } from './helper';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch surveys. * Handler to prefetch surveys.
@ -46,11 +47,13 @@ export class AddonModSurveyPrefetchHandler extends CoreCourseActivityPrefetchHan
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected surveyProvider: AddonModSurveyProvider, protected surveyProvider: AddonModSurveyProvider,
protected surveyHelper: AddonModSurveyHelperProvider, protected surveyHelper: AddonModSurveyHelperProvider,
protected injector: Injector) { 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 { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler';
import { AddonModUrlProvider } from './url'; import { AddonModUrlProvider } from './url';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; 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. * 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, filepoolProvider: CoreFilepoolProvider,
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, 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 { AddonModWikiProvider } from './wiki';
import { AddonModWikiSyncProvider } from './wiki-sync'; import { AddonModWikiSyncProvider } from './wiki-sync';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch wikis. * Handler to prefetch wikis.
@ -48,6 +49,7 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
protected wikiProvider: AddonModWikiProvider, protected wikiProvider: AddonModWikiProvider,
protected userProvider: CoreUserProvider, protected userProvider: CoreUserProvider,
protected textUtils: CoreTextUtilsProvider, protected textUtils: CoreTextUtilsProvider,
@ -56,7 +58,8 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl
protected gradesHelper: CoreGradesHelperProvider, protected gradesHelper: CoreGradesHelperProvider,
protected syncProvider: AddonModWikiSyncProvider) { 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(); siteId = this.sitesProvider.getCurrentSiteId();
promises.push(this.getFiles(module, courseId, single, siteId).then((files) => { 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) => { 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 { AddonModWorkshopSyncProvider } from './sync';
import { AddonModWorkshopHelperProvider } from './helper'; import { AddonModWorkshopHelperProvider } from './helper';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
/** /**
* Handler to prefetch workshops. * Handler to prefetch workshops.
@ -47,13 +48,15 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, domUtils: CoreDomUtilsProvider,
filterHelper: CoreFilterHelperProvider, filterHelper: CoreFilterHelperProvider,
pluginFileDelegate: CorePluginFileDelegate,
private groupsProvider: CoreGroupsProvider, private groupsProvider: CoreGroupsProvider,
private userProvider: CoreUserProvider, private userProvider: CoreUserProvider,
private workshopProvider: AddonModWorkshopProvider, private workshopProvider: AddonModWorkshopProvider,
private workshopHelper: AddonModWorkshopHelperProvider, private workshopHelper: AddonModWorkshopHelperProvider,
private syncProvider: AddonModWorkshopSyncProvider) { 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 { CoreRatingModule } from '@core/rating/rating.module';
import { CoreTagModule } from '@core/tag/tag.module'; import { CoreTagModule } from '@core/tag/tag.module';
import { CoreFilterModule } from '@core/filter/filter.module'; import { CoreFilterModule } from '@core/filter/filter.module';
import { CoreH5PModule } from '@core/h5p/h5p.module';
// Addon modules. // Addon modules.
import { AddonBadgesModule } from '@addon/badges/badges.module'; import { AddonBadgesModule } from '@addon/badges/badges.module';
@ -230,6 +231,7 @@ export const WP_PROVIDER: any = null;
CorePushNotificationsModule, CorePushNotificationsModule,
CoreTagModule, CoreTagModule,
CoreFilterModule, CoreFilterModule,
CoreH5PModule,
AddonBadgesModule, AddonBadgesModule,
AddonBlogModule, AddonBlogModule,
AddonCalendarModule, AddonCalendarModule,

View File

@ -359,6 +359,7 @@
"h261": {"type":"video/h261"}, "h261": {"type":"video/h261"},
"h263": {"type":"video/h263"}, "h263": {"type":"video/h263"},
"h264": {"type":"video/h264"}, "h264": {"type":"video/h264"},
"h5p": {"type":"application/zip","icon":"archive","string":"archive","groups":["archive"]},
"hal": {"type":"application/vnd.hal+xml"}, "hal": {"type":"application/vnd.hal+xml"},
"hbci": {"type":"application/vnd.hbci"}, "hbci": {"type":"application/vnd.hbci"},
"hdf": {"type":"application/x-hdf"}, "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.group": "Group",
"core.groupsseparate": "Separate groups", "core.groupsseparate": "Separate groups",
"core.groupsvisible": "Visible 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.hasdatatosync": "This {{$a}} has offline data to be synchronised.",
"core.help": "Help", "core.help": "Help",
"core.hide": "Hide", "core.hide": "Hide",

View File

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

View File

@ -23,6 +23,7 @@ import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreConstants } from '@core/constants'; 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 * 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 timemodified: number;
protected observer; protected observer;
constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider, constructor(private sitesProvider: CoreSitesProvider,
private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider,
private fileHelper: CoreFileHelperProvider, private mimeUtils: CoreMimetypeUtilsProvider, private domUtils: CoreDomUtilsProvider,
private eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider) { 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(); this.onDelete = new EventEmitter();
} }
@ -141,8 +148,6 @@ export class CoreFileComponent implements OnInit, OnDestroy {
e && e.preventDefault(); e && e.preventDefault();
e && e.stopPropagation(); e && e.stopPropagation();
let promise;
if (this.isDownloading && !openAfterDownload) { if (this.isDownloading && !openAfterDownload) {
return; return;
} }
@ -164,7 +169,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
} }
if (!this.appProvider.isOnline() && (!openAfterDownload || (openAfterDownload && 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); this.domUtils.showErrorModal('core.networkerrormsg', true);
return; return;
@ -177,20 +182,26 @@ export class CoreFileComponent implements OnInit, OnDestroy {
}); });
} else { } else {
// File doesn't need to be opened (it's a prefetch). Show confirm modal if file size is defined and it's big. // 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(); this.pluginFileDelegate.getFileSize({fileurl: this.fileUrl, filesize: this.fileSize}, this.siteId).then((size) => {
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, const promise = size ? this.domUtils.confirmDownloadSize({ size: size, total: true }) : Promise.resolve();
this.componentId, this.timemodified, undefined, undefined, 0, this.file).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); return promise.then(() => {
this.calculateState(); // 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(() => { }).catch((error) => {
// Ignore 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> <iframe #iframe [hidden]="loading" class="core-iframe" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}" [src]="safeUrl"></iframe>
<span class="core-loading-spinner"> <span class="core-loading-spinner">
<ion-spinner *ngIf="loading"></ion-spinner> <ion-spinner *ngIf="loading"></ion-spinner>

View File

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

View File

@ -51,7 +51,7 @@ export class CoreSitePickerComponent implements OnInit {
sites.forEach((site: any) => { sites.forEach((site: any) => {
// Format the site name. // Format the site name.
promises.push(this.filterProvider.formatText(site.siteName, {clean: true, singleLine: true, filter: false}, [], promises.push(this.filterProvider.formatText(site.siteName, {clean: true, singleLine: true, filter: false}, [],
site.getId()).catch(() => { site.id).catch(() => {
return site.siteName; return site.siteName;
}).then((siteName) => { }).then((siteName) => {
site.fullNameAndSiteName = this.translate.instant('core.fullnameandsitename', 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_FILEUPLOADER_PROVIDERS } from '@core/fileuploader/fileuploader.module';
import { CORE_FILTER_PROVIDERS } from '@core/filter/filter.module'; import { CORE_FILTER_PROVIDERS } from '@core/filter/filter.module';
import { CORE_GRADES_PROVIDERS } from '@core/grades/grades.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_LOGIN_PROVIDERS } from '@core/login/login.module';
import { CORE_MAINMENU_PROVIDERS } from '@core/mainmenu/mainmenu.module'; import { CORE_MAINMENU_PROVIDERS } from '@core/mainmenu/mainmenu.module';
import { CORE_QUESTION_PROVIDERS } from '@core/question/question.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_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(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_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. // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
for (const i in providers) { for (const i in providers) {

View File

@ -21,6 +21,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '../providers/course'; import { CoreCourseProvider } from '../providers/course';
import { CoreCourseModulePrefetchHandler } from '../providers/module-prefetch-delegate'; import { CoreCourseModulePrefetchHandler } from '../providers/module-prefetch-delegate';
import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; 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 * 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 filepoolProvider: CoreFilepoolProvider,
protected sitesProvider: CoreSitesProvider, protected sitesProvider: CoreSitesProvider,
protected domUtils: CoreDomUtilsProvider, 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. * 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 }> { getDownloadSize(module: any, courseId: number, single?: boolean): Promise<{ size: number, total: boolean }> {
return this.getFiles(module, courseId).then((files) => { return this.getFiles(module, courseId).then((files) => {
return this.utils.sumFileSizes(files); return this.pluginFileDelegate.getFilesSize(files);
}).catch(() => { }).catch(() => {
return { size: -1, total: false }; return { size: -1, total: false };
}); });
@ -193,12 +195,12 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref
if (typeof instance.introfiles != 'undefined') { if (typeof instance.introfiles != 'undefined') {
return instance.introfiles; return instance.introfiles;
} else if (instance.intro) { } else if (instance.intro) {
return this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(instance.intro); return this.filepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects(instance.intro);
} }
} }
if (module.description) { if (module.description) {
return this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(module.description); return this.filepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects(module.description);
} }
return []; return [];

View File

@ -438,7 +438,7 @@ export class CoreCourseHelperProvider {
sizePromise = this.prefetchDelegate.getDownloadSize(section.modules, courseId); sizePromise = this.prefetchDelegate.getDownloadSize(section.modules, courseId);
// Check if the section has embedded files in the description. // 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 { } else {
const promises = [], const promises = [],
results = { results = {
@ -454,7 +454,7 @@ export class CoreCourseHelperProvider {
})); }));
// Check if the section has embedded files in the description. // 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; haveEmbeddedFiles = true;
} }
} }
@ -1089,7 +1089,7 @@ export class CoreCourseHelperProvider {
// Get the time it was downloaded (if it was downloaded). // Get the time it was downloaded (if it was downloaded).
promises.push(this.filepoolProvider.getPackageData(siteId, component, module.id).then((data) => { 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(); const now = this.timeUtils.timestamp();
moduleInfo.downloadTime = data.downloadTime; moduleInfo.downloadTime = data.downloadTime;
if (now - data.downloadTime < 7 * 86400) { if (now - data.downloadTime < 7 * 86400) {
@ -1449,7 +1449,7 @@ export class CoreCourseHelperProvider {
})); }));
// Download the files in the section description. // 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(); siteId = this.sitesProvider.getCurrentSiteId();
promises.push(this.filepoolProvider.addFilesToQueue(siteId, introFiles, CoreCourseProvider.COMPONENT, courseId) 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 { Md5 } from 'ts-md5/dist/md5';
import { Subject, BehaviorSubject, Subscription } from 'rxjs'; import { Subject, BehaviorSubject, Subscription } from 'rxjs';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { CoreFileHelperProvider } from '@providers/file-helper';
/** /**
* Progress of downloading a list of modules. * 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, constructor(loggerProvider: CoreLoggerProvider,
private courseProvider: CoreCourseProvider, private filepoolProvider: CoreFilepoolProvider, protected sitesProvider: CoreSitesProvider,
private timeUtils: CoreTimeUtilsProvider, private fileProvider: CoreFileProvider, protected utils: CoreUtilsProvider,
protected eventsProvider: CoreEventsProvider) { protected courseProvider: CoreCourseProvider,
protected filepoolProvider: CoreFilepoolProvider,
protected timeUtils: CoreTimeUtilsProvider,
protected fileProvider: CoreFileProvider,
protected eventsProvider: CoreEventsProvider,
protected fileHelper: CoreFileHelperProvider) {
super('CoreCourseModulePrefetchDelegate', loggerProvider, sitesProvider, eventsProvider); super('CoreCourseModulePrefetchDelegate', loggerProvider, sitesProvider, eventsProvider);
this.sitesProvider.registerSiteSchema(this.siteSchema); this.sitesProvider.registerSiteSchema(this.siteSchema);
@ -881,7 +887,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
const packageId = this.filepoolProvider.getPackageId(handler.component, module.id), const packageId = this.filepoolProvider.getPackageId(handler.component, module.id),
status = this.statusCache.getValue(packageId, 'status'); 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. // Module isn't downloaded, just return the status.
return Promise.resolve({ return Promise.resolve({
status: status status: status
@ -927,7 +933,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
// Get the status and download time of the module. // Get the status and download time of the module.
return this.getModuleStatusAndDownloadTime(module, courseId).then((data) => { 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. // Not downloaded, no updates.
return {}; return {};
} }

View File

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

View File

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

View File

@ -402,7 +402,7 @@ export type CoreFilterFilter = {
*/ */
export type CoreFilterGetAvailableInContextResult = { export type CoreFilterGetAvailableInContextResult = {
filters: CoreFilterFilter[]; // Available filters. 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) prefetchQuestionFiles(question: any, component?: string, componentId?: string | number, siteId?: string, usageId?: number)
: Promise<any> { : Promise<any> {
const urls = this.domUtils.extractDownloadableFilesFromHtml(question.html); const urls = this.filepoolProvider.extractDownloadableFilesFromHtml(question.html);
if (!component) { if (!component) {
component = CoreQuestionProvider.COMPONENT; component = CoreQuestionProvider.COMPONENT;

View File

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

View File

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

View File

@ -12,7 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { Platform, NavController, Content } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
@ -90,7 +92,8 @@ export class CoreFormatTextDirective implements OnChanges {
private eventsProvider: CoreEventsProvider, private eventsProvider: CoreEventsProvider,
private filterProvider: CoreFilterProvider, private filterProvider: CoreFilterProvider,
private filterHelper: CoreFilterHelperProvider, private filterHelper: CoreFilterHelperProvider,
private filterDelegate: CoreFilterDelegate) { private filterDelegate: CoreFilterDelegate,
private viewContainerRef: ViewContainerRef) {
this.element = element.nativeElement; this.element = element.nativeElement;
this.element.classList.add('opacity-hide'); // Hide contents until they're treated. 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) { 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. // 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'); 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. // Error getting the site. This probably means that there is no current site and no siteId was supplied.
}).then((siteInstance: CoreSite) => { }).then((siteInstance: CoreSite) => {
site = siteInstance; site = siteInstance;
result.siteId = site.getId();
if (site) {
result.siteId = site.getId();
}
if (this.contextLevel == 'course' && this.contextInstanceId <= 0) { if (this.contextLevel == 'course' && this.contextInstanceId <= 0) {
this.contextInstanceId = site.getSiteHomeId(); this.contextInstanceId = site.getSiteHomeId();
@ -418,14 +425,14 @@ export class CoreFormatTextDirective implements OnChanges {
if (this.filter) { if (this.filter) {
return this.filterHelper.getFiltersAndFormatText(this.text, this.contextLevel, this.contextInstanceId, 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; result.filters = res.filters;
return res.text; return res.text;
}); });
} else { } else {
return this.filterProvider.formatText(this.text, result.options, [], site.getId()); return this.filterProvider.formatText(this.text, result.options, [], result.siteId);
} }
}).then((formatted) => { }).then((formatted) => {

View File

@ -52,6 +52,7 @@ export class CoreFileProvider {
static FORMATDATAURL = 1; static FORMATDATAURL = 1;
static FORMATBINARYSTRING = 2; static FORMATBINARYSTRING = 2;
static FORMATARRAYBUFFER = 3; static FORMATARRAYBUFFER = 3;
static FORMATJSON = 4;
// Folders. // Folders.
static SITESFOLDER = 'sites'; static SITESFOLDER = 'sites';
@ -491,6 +492,7 @@ export class CoreFileProvider {
* FORMATDATAURL * FORMATDATAURL
* FORMATBINARYSTRING * FORMATBINARYSTRING
* FORMATARRAYBUFFER * FORMATARRAYBUFFER
* FORMATJSON
* @return Promise to be resolved when the file is read. * @return Promise to be resolved when the file is read.
*/ */
readFile(path: string, format: number = CoreFileProvider.FORMATTEXT): Promise<any> { readFile(path: string, format: number = CoreFileProvider.FORMATTEXT): Promise<any> {
@ -505,6 +507,16 @@ export class CoreFileProvider {
return this.file.readAsBinaryString(this.basePath, path); return this.file.readAsBinaryString(this.basePath, path);
case CoreFileProvider.FORMATARRAYBUFFER: case CoreFileProvider.FORMATARRAYBUFFER:
return this.file.readAsArrayBuffer(this.basePath, path); 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: default:
return this.file.readAsText(this.basePath, path); return this.file.readAsText(this.basePath, path);
} }
@ -519,6 +531,7 @@ export class CoreFileProvider {
* FORMATDATAURL * FORMATDATAURL
* FORMATBINARYSTRING * FORMATBINARYSTRING
* FORMATARRAYBUFFER * FORMATARRAYBUFFER
* FORMATJSON
* @return Promise to be resolved when the file is read. * @return Promise to be resolved when the file is read.
*/ */
readFileData(fileData: any, format: number = CoreFileProvider.FORMATTEXT): Promise<any> { readFileData(fileData: any, format: number = CoreFileProvider.FORMATTEXT): Promise<any> {
@ -531,7 +544,18 @@ export class CoreFileProvider {
reader.onloadend = (evt): void => { reader.onloadend = (evt): void => {
const target = <any> evt.target; // Convert to <any> to be able to use non-standard properties. const target = <any> evt.target; // Convert to <any> to be able to use non-standard properties.
if (target.result !== undefined || target.result !== null) { 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) { } else if (target.error !== undefined || target.error !== null) {
reject(target.error); reject(target.error);
} else { } 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. * Move a file.
* *
* @param originalPath Path to the file to move. * @param originalPath Path to the file to move.
* @param newPath New path of the file. * @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. * @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(() => { return this.init().then(() => {
// Remove basePath if it's in the paths. // Remove basePath if it's in the paths.
originalPath = this.removeStartingSlash(originalPath.replace(this.basePath, '')); originalPath = this.removeStartingSlash(originalPath.replace(this.basePath, ''));
newPath = this.removeStartingSlash(newPath.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) { if (this.isHTMLAPI) {
// In Cordova API we need to calculate the longest matching path to make it work. // 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. // 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 { } 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. // The move can fail if the path has encoded characters. Try again if that's the case.
const decodedOriginal = decodeURI(originalPath), const decodedOriginal = decodeURI(originalPath),
decodedNew = decodeURI(newPath); decodedNew = decodeURI(newPath);
if (decodedOriginal != originalPath || decodedNew != 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 { } else {
return Promise.reject(error); 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. * Copy a file.
* *
* @param from Path to the file to move. * @param from Path to the file to move.
* @param to New path of the file. * @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. * @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, let fromFileAndDir,
toFileAndDir; toFileAndDir;
const copyFn = isDir ? this.file.copyDir.bind(this.file) : this.file.copyFile.bind(this.file);
return this.init().then(() => { return this.init().then(() => {
// Paths cannot start with "/". Remove basePath if present. // Paths cannot start with "/". Remove basePath if present.
@ -799,7 +892,7 @@ export class CoreFileProvider {
fromFileAndDir = this.getFileAndDirectoryFromPath(from); fromFileAndDir = this.getFileAndDirectoryFromPath(from);
toFileAndDir = this.getFileAndDirectoryFromPath(to); toFileAndDir = this.getFileAndDirectoryFromPath(to);
if (toFileAndDir.directory) { if (toFileAndDir.directory && !destDirExists) {
// Create the target directory if it doesn't exist. // Create the target directory if it doesn't exist.
return this.createDir(toFileAndDir.directory); return this.createDir(toFileAndDir.directory);
} }
@ -809,15 +902,15 @@ export class CoreFileProvider {
const fromDir = this.textUtils.concatenatePaths(this.basePath, fromFileAndDir.directory), const fromDir = this.textUtils.concatenatePaths(this.basePath, fromFileAndDir.directory),
toDir = this.textUtils.concatenatePaths(this.basePath, toFileAndDir.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 { } 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. // The copy can fail if the path has encoded characters. Try again if that's the case.
const decodedFrom = decodeURI(from), const decodedFrom = decodeURI(from),
decodedTo = decodeURI(to); decodedTo = decodeURI(to);
if (from != decodedFrom || to != decodedTo) { if (from != decodedFrom || to != decodedTo) {
return this.file.copyFile(this.basePath, decodedFrom, this.basePath, decodedTo); return copyFn(this.basePath, decodedFrom, this.basePath, decodedTo);
} else { } else {
return Promise.reject(error); 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 * @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). * same location and name as the ZIP file (without extension).
* @param onProgress Function to call on progress. * @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. * @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. // 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). // 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)); destFolder = this.addBasePathIfNeeded(destFolder || this.mimeUtils.removeExtension(path));
@ -1146,4 +1254,19 @@ export class CoreFileProvider {
isFileInAppFolder(path: string): boolean { isFileInAppFolder(path: string): boolean {
return path.indexOf(this.basePath) != -1; 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 { CoreLoggerProvider } from './logger';
import { CorePluginFileDelegate } from './plugin-file-delegate'; import { CorePluginFileDelegate } from './plugin-file-delegate';
import { CoreSitesProvider, CoreSiteSchema } from './sites'; import { CoreSitesProvider, CoreSiteSchema } from './sites';
import { CoreWSProvider } from './ws'; import { CoreWSProvider, CoreWSExternalFile } from './ws';
import { CoreDomUtilsProvider } from './utils/dom'; import { CoreDomUtilsProvider } from './utils/dom';
import { CoreMimetypeUtilsProvider } from './utils/mimetype'; import { CoreMimetypeUtilsProvider } from './utils/mimetype';
import { CoreTextUtilsProvider } from './utils/text'; 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. * 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> { addFileLinkByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number): Promise<any> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => { return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(fileUrl); const fileId = this.getFileIdByUrl(file.fileurl);
return this.addFileLink(siteId, fileId, component, componentId); 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 priority The priority this file should get in the queue (range 0-999).
* @param options Extra options (isexternalfile, repositorytype). * @param options Extra options (isexternalfile, repositorytype).
* @param revision File revision. If not defined, it will be calculated using the URL. * @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. * @return Resolved on success.
*/ */
addToQueueByUrl(siteId: string, fileUrl: string, component?: string, componentId?: string | number, timemodified: number = 0, 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) filePath?: string, onProgress?: (event: any) => any, priority: number = 0, options: any = {}, revision?: number,
: Promise<any> { alreadyFixed?: boolean): Promise<any> {
let fileId, let fileId,
link, link,
queueDeferred; queueDeferred;
@ -623,94 +624,102 @@ export class CoreFilepoolProvider {
return Promise.reject(null); return Promise.reject(null);
} }
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => { if (alreadyFixed) {
const primaryKey = { siteId: siteId, fileId: fileId }; // 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); fileUrl = file.fileurl;
fileId = this.getFileIdByUrl(fileUrl); timemodified = file.timemodified || timemodified;
revision = revision || this.getRevisionFromUrl(fileUrl);
fileId = this.getFileIdByUrl(fileUrl);
// Set up the component. const primaryKey = { siteId: siteId, fileId: fileId };
if (typeof component != 'undefined') {
link = {
component: component,
componentId: this.fixComponentId(componentId)
};
}
// Retrieve the queue deferred now if it exists. // Set up the component.
// This is to prevent errors if file is removed from queue while we're checking if the file is in queue. if (typeof component != 'undefined') {
queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress); link = {
component: component,
componentId: this.fixComponentId(componentId)
};
}
return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => { // Retrieve the queue deferred now if it exists.
const newData: any = {}; // This is to prevent errors if file is removed from queue while we're checking if the file is in queue.
let foundLink = false; queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress);
if (entry) { return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => {
// We already have the file in queue, we update the priority and links. const newData: any = {};
if (entry.priority < priority) { let foundLink = false;
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) { if (entry) {
// We need to add the new link if it does not exist yet. // We already have the file in queue, we update the priority and links.
if (entry.links && entry.links.length) { if (entry.priority < priority) {
for (const i in entry.links) { newData.priority = priority;
const fileLink = entry.links[i]; }
if (fileLink.component == link.component && fileLink.componentId == link.componentId) { if (revision && entry.revision !== revision) {
foundLink = true; newData.revision = revision;
break; }
} 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) { if (!foundLink) {
// Update only when required. newData.links = entry.links || [];
this.logger.debug(`Updating file ${fileId} which is already in queue`); newData.links.push(link);
newData.links = JSON.stringify(entry.links);
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. 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( return this.addToQueue(
siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link); 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. * Adds a file to the queue if the size is allowed to be downloaded.
* *
* @param siteId The site ID. * @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 component The component to link the file to.
* @param componentId An ID to use in conjunction with the component. * @param componentId An ID to use in conjunction with the component.
* @param timemodified The time this file was modified. * @param timemodified The time this file was modified.
@ -760,18 +769,18 @@ export class CoreFilepoolProvider {
// Check if the file should be downloaded. // Check if the file should be downloaded.
if (sizeUnknown) { if (sizeUnknown) {
if (downloadUnknown && isWifi) { if (downloadUnknown && isWifi) {
return this.addToQueueByUrl( return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined,
siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, revision); 0, options, revision, true);
} }
} else if (size <= this.DOWNLOAD_THRESHOLD || (isWifi && size <= this.WIFI_DOWNLOAD_THRESHOLD)) { } else if (this.shouldDownload(size)) {
return this.addToQueueByUrl( return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0,
siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, revision); options, revision, true);
} }
}); });
} else { } else {
// No need to check size, just add it to the queue. // No need to check size, just add it to the queue.
return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, 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 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 || {}; const data: CoreFilepoolFileEntry = poolFileObject || {};
data.downloadTime = Date.now(); data.downloadTime = Date.now();
@ -1157,8 +1172,10 @@ export class CoreFilepoolProvider {
promise; promise;
if (this.fileProvider.isAvailable()) { if (this.fileProvider.isAvailable()) {
return this.fixPluginfileURL(siteId, fileUrl).then((fixedUrl) => { return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
fileUrl = fixedUrl;
fileUrl = file.fileurl;
timemodified = file.timemodified || timemodified;
options = Object.assign({}, options); // Create a copy to prevent modifying the original object. options = Object.assign({}, options); // Create a copy to prevent modifying the original object.
options.timemodified = timemodified || 0; 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. * Fill Missing Extension In the File Object if needed.
* This is to migrate from old versions. * 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 siteId The site ID.
* @param fileUrl The file URL. * @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> { protected fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise<CoreWSExternalFile> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.checkAndFixPluginfileURL(fileUrl); 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> { getDirectoryUrlByUrl(siteId: string, fileUrl: string): Promise<string> {
if (this.fileProvider.isAvailable()) { if (this.fileProvider.isAvailable()) {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => { return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(fileUrl), const fileId = this.getFileIdByUrl(file.fileurl),
filePath = <string> this.getFilePath(siteId, fileId, ''); // No extension, the function will return a string. filePath = <string> this.getFilePath(siteId, fileId, ''); // No extension, the function will return a string.
return this.fileProvider.getDir(filePath).then((dirEntry) => { return this.fileProvider.getDir(filePath).then((dirEntry) => {
@ -1394,8 +1473,8 @@ export class CoreFilepoolProvider {
* @return Promise resolved with event name. * @return Promise resolved with event name.
*/ */
getFileEventNameByUrl(siteId: string, fileUrl: string): Promise<string> { getFileEventNameByUrl(siteId: string, fileUrl: string): Promise<string> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => { return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(fileUrl); const fileId = this.getFileIdByUrl(file.fileurl);
return this.getFileEventName(siteId, fileId); 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. * @return Promise resolved with the path to the file relative to storage root.
*/ */
getFilePathByUrl(siteId: string, fileUrl: string): Promise<string> { getFilePathByUrl(siteId: string, fileUrl: string): Promise<string> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => { return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(fileUrl); const fileId = this.getFileIdByUrl(file.fileurl);
return this.getFilePath(siteId, fileId); return this.getFilePath(siteId, fileId);
}); });
@ -1587,8 +1666,10 @@ export class CoreFilepoolProvider {
: Promise<string> { : Promise<string> {
let fileId; let fileId;
return this.fixPluginfileURL(siteId, fileUrl).then((fixedUrl) => { return this.fixPluginfileURL(siteId, fileUrl, timemodified).then((file) => {
fileUrl = fixedUrl;
fileUrl = file.fileurl;
timemodified = file.timemodified || timemodified;
revision = revision || this.getRevisionFromUrl(fileUrl); revision = revision || this.getRevisionFromUrl(fileUrl);
fileId = this.getFileIdByUrl(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) => { return this.fixPluginfileURL(siteId, fileUrl, timemodified).then((file) => {
fileUrl = fixedUrl;
fileUrl = file.fileurl;
timemodified = file.timemodified || timemodified;
revision = revision || this.getRevisionFromUrl(fileUrl); revision = revision || this.getRevisionFromUrl(fileUrl);
fileId = this.getFileIdByUrl(fileUrl); fileId = this.getFileIdByUrl(fileUrl);
@ -1779,8 +1864,8 @@ export class CoreFilepoolProvider {
*/ */
getInternalUrlByUrl(siteId: string, fileUrl: string): Promise<string> { getInternalUrlByUrl(siteId: string, fileUrl: string): Promise<string> {
if (this.fileProvider.isAvailable()) { if (this.fileProvider.isAvailable()) {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => { return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(fileUrl); const fileId = this.getFileIdByUrl(file.fileurl);
return this.getInternalUrlById(siteId, fileId); return this.getInternalUrlById(siteId, fileId);
}); });
@ -1843,8 +1928,8 @@ export class CoreFilepoolProvider {
* @return Promise resolved with the path of the package. * @return Promise resolved with the path of the package.
*/ */
getPackageDirPathByUrl(siteId: string, url: string): Promise<string> { getPackageDirPathByUrl(siteId: string, url: string): Promise<string> {
return this.fixPluginfileURL(siteId, url).then((fixedUrl) => { return this.fixPluginfileURL(siteId, url).then((file) => {
const dirName = this.getPackageDirNameByUrl(fixedUrl); const dirName = this.getPackageDirNameByUrl(file.fileurl);
return this.getFilePath(siteId, dirName, ''); return this.getFilePath(siteId, dirName, '');
}); });
@ -1859,8 +1944,8 @@ export class CoreFilepoolProvider {
*/ */
getPackageDirUrlByUrl(siteId: string, url: string): Promise<string> { getPackageDirUrlByUrl(siteId: string, url: string): Promise<string> {
if (this.fileProvider.isAvailable()) { if (this.fileProvider.isAvailable()) {
return this.fixPluginfileURL(siteId, url).then((fixedUrl) => { return this.fixPluginfileURL(siteId, url).then((file) => {
const dirName = this.getPackageDirNameByUrl(fixedUrl), const dirName = this.getPackageDirNameByUrl(file.fileurl),
dirPath = <string> this.getFilePath(siteId, dirName, ''); // No extension, the function will return a string. dirPath = <string> this.getFilePath(siteId, dirName, ''); // No extension, the function will return a string.
return this.fileProvider.getDir(dirPath).then((dirEntry) => { 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. * 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> { invalidateFileByUrl(siteId: string, fileUrl: string): Promise<any> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => { return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(fileUrl); const fileId = this.getFileIdByUrl(file.fileurl);
return this.sitesProvider.getSiteDb(siteId).then((db) => { return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.updateRecords(this.FILES_TABLE, { stale: 1 }, { fileId: fileId }); 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. * @param Promise resolved if file is downloading, rejected otherwise.
*/ */
isFileDownloadingByUrl(siteId: string, fileUrl: string): Promise<any> { isFileDownloadingByUrl(siteId: string, fileUrl: string): Promise<any> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => { return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(fileUrl); const fileId = this.getFileIdByUrl(file.fileurl);
return this.hasFileInQueue(siteId, fileId); return this.hasFileInQueue(siteId, fileId);
}); });
@ -2614,7 +2699,22 @@ export class CoreFilepoolProvider {
protected removeFileById(siteId: string, fileId: string): Promise<any> { protected removeFileById(siteId: string, fileId: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => { 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. // 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 = []; const promises = [];
// Remove entry from filepool store. // Remove entry from filepool store.
@ -2636,6 +2736,10 @@ export class CoreFilepoolProvider {
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
this.notifyFileDeleted(siteId, fileId); 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. * @return Resolved on success, rejected on failure.
*/ */
removeFileByUrl(siteId: string, fileUrl: string): Promise<any> { removeFileByUrl(siteId: string, fileUrl: string): Promise<any> {
return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => { return this.fixPluginfileURL(siteId, fileUrl).then((file) => {
const fileId = this.getFileIdByUrl(fileUrl); const fileId = this.getFileIdByUrl(file.fileurl);
return this.removeFileById(siteId, fileId); 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. * Convenience function to check if a file should be downloaded before opening it.
* *

View File

@ -13,21 +13,23 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreEventsProvider } from './events';
import { CoreLoggerProvider } from './logger'; 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. * Interface that all plugin file handlers must implement.
*/ */
export interface CorePluginFileHandler { export interface CorePluginFileHandler extends CoreDelegateHandler {
/**
* A name to identify the handler.
*/
name: string;
/** /**
* The "component" of the handler. It should match the "component" of pluginfile URLs. * 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. * 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. * @return String to remove the revision on pluginfile url.
*/ */
getComponentRevisionReplace?(args: string[]): string; 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. * Delegate to register pluginfile information handlers.
*/ */
@Injectable() @Injectable()
export class CorePluginFileDelegate { export class CorePluginFileDelegate extends CoreDelegate {
protected logger; protected handlerNameProperty = 'component';
protected handlers: { [s: string]: CorePluginFileHandler } = {};
constructor(logger: CoreLoggerProvider) { constructor(loggerProvider: CoreLoggerProvider,
this.logger = logger.getInstance('CorePluginFileDelegate'); 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. * @param fileUrl The file URL used to download the file.
* @return Handler. Undefined if no handler found for the plugin. * @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 { fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<any> {
if (typeof this.handlers[component] != 'undefined') { const handler = this.getHandlerForFile({fileurl: fileUrl});
return this.handlers[component];
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 { getComponentRevisionRegExp(args: string[]): RegExp {
// Get handler based on component (args[1]). // 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) { if (handler && handler.getComponentRevisionRegExp) {
return handler.getComponentRevisionRegExp(args); 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. * @param container Container where to get the URLs from.
* @return True if registered successfully, false otherwise. * @return List of URLs.
*/ */
registerHandler(handler: CorePluginFileHandler): boolean { getDownloadableFilesFromHTML(container: HTMLElement): string[] {
if (typeof this.handlers[handler.component] !== 'undefined') { let files = [];
this.logger.log(`Handler '${handler.component}' already registered`);
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}'`); return files;
this.handlers[handler.component] = handler; }
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 { removeRevisionFromUrl(url: string, args: string[]): string {
// Get handler based on component (args[1]). // 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) { if (handler && handler.getComponentRevisionRegExp && handler.getComponentRevisionReplace) {
const revisionRegex = handler.getComponentRevisionRegExp(args); const revisionRegex = handler.getComponentRevisionRegExp(args);
@ -124,4 +295,22 @@ export class CorePluginFileDelegate {
return url; 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 { CoreTextUtilsProvider } from './text';
import { CoreAppProvider } from '../app'; import { CoreAppProvider } from '../app';
import { CoreConfigProvider } from '../config'; import { CoreConfigProvider } from '../config';
import { CoreLoggerProvider } from '../logger';
import { CoreUrlUtilsProvider } from './url'; import { CoreUrlUtilsProvider } from './url';
import { CoreFileProvider } from '@providers/file'; import { CoreFileProvider } from '@providers/file';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
@ -61,12 +62,24 @@ export class CoreDomUtilsProvider {
protected lastInstanceId = 0; protected lastInstanceId = 0;
protected debugDisplay = false; // Whether to display debug messages. Store it in a variable to make it synchronous. protected debugDisplay = false; // Whether to display debug messages. Store it in a variable to make it synchronous.
protected displayedAlerts = {}; // To prevent duplicated alerts. protected displayedAlerts = {}; // To prevent duplicated alerts.
protected logger;
constructor(private translate: TranslateService, private loadingCtrl: LoadingController, private toastCtrl: ToastController, constructor(private translate: TranslateService,
private alertCtrl: AlertController, private textUtils: CoreTextUtilsProvider, private appProvider: CoreAppProvider, private loadingCtrl: LoadingController,
private platform: Platform, private configProvider: CoreConfigProvider, private urlUtils: CoreUrlUtilsProvider, private toastCtrl: ToastController,
private modalCtrl: ModalController, private sanitizer: DomSanitizer, private popoverCtrl: PopoverController, private alertCtrl: AlertController,
private fileProvider: CoreFileProvider) { 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. // Check if debug messages should be displayed.
configProvider.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, false).then((debugDisplay) => { configProvider.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, false).then((debugDisplay) => {
@ -250,8 +263,12 @@ export class CoreDomUtilsProvider {
* *
* @param html HTML code. * @param html HTML code.
* @return List of file urls. * @return List of file urls.
* @deprecated since 3.8. Use CoreFilepoolProvider.extractDownloadableFilesFromHtml instead.
*/ */
extractDownloadableFilesFromHtml(html: string): string[] { 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 = []; const urls = [];
let elements; let elements;
@ -283,6 +300,7 @@ export class CoreDomUtilsProvider {
* *
* @param html HTML code. * @param html HTML code.
* @return List of fake file objects with file URLs. * @return List of fake file objects with file URLs.
* @deprecated since 3.8. Use CoreFilepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects instead.
*/ */
extractDownloadableFilesFromHtmlAsFakeFileObjects(html: string): any[] { extractDownloadableFilesFromHtmlAsFakeFileObjects(html: string): any[] {
const urls = this.extractDownloadableFilesFromHtml(html); const urls = this.extractDownloadableFilesFromHtml(html);
@ -372,7 +390,7 @@ export class CoreDomUtilsProvider {
* @return Formatted size. If size is not valid, returns an empty string. * @return Formatted size. If size is not valid, returns an empty string.
*/ */
formatPixelsSize(size: any): 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. // It seems to be a valid size.
return size; return size;
} }

View File

@ -319,58 +319,86 @@ export class CoreIframeUtilsProvider {
while (el && el.tagName !== 'A') { while (el && el.tagName !== 'A') {
el = el.parentElement; el = el.parentElement;
} }
if (!el || el.tagName !== 'A') {
return;
}
const link = <HTMLAnchorElement> el;
const scheme = this.urlUtils.getUrlScheme(link.href); const link = <CoreIframeHTMLAnchorElement> el;
if (!link.href || (scheme && scheme == 'javascript')) { if (!link || link.treated) {
// Links with no URL and Javascript links are ignored.
return; return;
} }
if (scheme && scheme != 'file' && scheme != 'filesystem') { // Add click listener to the link, this way if the iframe has added a listener to the link it will be executed first.
// Scheme suggests it's an external resource. link.treated = true;
event.preventDefault(); 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. const scheme = this.urlUtils.getUrlScheme(link.href);
if (frameScheme && frameScheme != 'file' && frameScheme != 'filesystem' && if (!link.href || (scheme && scheme == 'javascript')) {
(!link.target || link.target == '_self')) { // Links with no URL and Javascript links are ignored.
// Load the link inside the frame itself. return;
if (element.tagName.toLowerCase() == 'object') { }
element.setAttribute('data', link.href);
} else {
element.setAttribute('src', link.href);
}
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. const frameSrc = (<HTMLFrameElement> element).src || (<HTMLObjectElement> element).data,
if (!this.sitesProvider.isLoggedIn()) { frameScheme = this.urlUtils.getUrlScheme(frameSrc);
this.utils.openInBrowser(link.href);
} else { // If the frame is not local, check the target to identify how to treat the link.
this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(link.href); if (frameScheme && frameScheme != 'file' && frameScheme != 'filesystem' &&
} (!link.target || link.target == '_self')) {
} else if (link.target == '_parent' || link.target == '_top' || link.target == '_blank') { // Load the link inside the frame itself.
// 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') { if (element.tagName.toLowerCase() == 'object') {
element.setAttribute('data', link.href); element.setAttribute('data', link.href);
} else { } else {
element.setAttribute('src', link.href); 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; return true;
} }
const div = document.createElement('div'); this.template.innerHTML = content;
div.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