commit
a8ee898a91
|
@ -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/'
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"},
|
||||||
|
|
|
@ -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 |
|
@ -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",
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 [];
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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.
|
@ -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=" " horiz-adv-x="512" d="" />
|
||||||
|
<glyph unicode="" glyph-name="arrow-down" data-tags="arrow-down" d="M234 389.669h556l-278-278z" />
|
||||||
|
<glyph unicode="" glyph-name="arrow-left" data-tags="arrow-left" d="M381-11.331v524l262-262z" />
|
||||||
|
<glyph unicode="" glyph-name="colapse" data-tags="colapse" d="M512 447.336l256-256-60-60-196 196-196-196-60 60z" />
|
||||||
|
<glyph unicode="" glyph-name="expand" data-tags="expand" d="M708 423.336l60-60-256-256-256 256 60 60 196-196z" />
|
||||||
|
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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.
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
||||||
|
};
|
|
@ -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);
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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();
|
|
@ -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;
|
||||||
|
})();
|
|
@ -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);
|
|
@ -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);
|
|
@ -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, '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
|
@ -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: '<',
|
||||||
|
'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: '>',
|
||||||
|
'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);
|
|
@ -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;
|
||||||
|
})();
|
|
@ -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'
|
||||||
|
];
|
|
@ -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
|
@ -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);
|
|
@ -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);
|
|
@ -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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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');
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
@ -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 {}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
};
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue