diff --git a/config/webpack.config.js b/config/webpack.config.js index 2118f9cb2..203780578 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -6,12 +6,13 @@ const customConfig = { resolve: { alias: { '@addon': resolve('./src/addon'), + '@app': resolve('./src/app'), '@classes': resolve('./src/classes'), '@core': resolve('./src/core'), '@providers': resolve('./src/providers'), '@components': resolve('./src/components'), - '@directives': resolve('./src/directives/directives.module'), - '@pipes': resolve('./src/pipes/pipes.module') + '@directives': resolve('./src/directives'), + '@pipes': resolve('./src/pipes') } } }; diff --git a/src/addon/calendar/pages/event/event.module.ts b/src/addon/calendar/pages/event/event.module.ts index d579114cc..fe5029958 100644 --- a/src/addon/calendar/pages/event/event.module.ts +++ b/src/addon/calendar/pages/event/event.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; -import { CorePipesModule } from '@pipes'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; import { AddonCalendarEventPage } from './event'; @NgModule({ diff --git a/src/addon/calendar/pages/list/list.module.ts b/src/addon/calendar/pages/list/list.module.ts index b685b6578..bfe0f7ecd 100644 --- a/src/addon/calendar/pages/list/list.module.ts +++ b/src/addon/calendar/pages/list/list.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; -import { CorePipesModule } from '@pipes'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; import { AddonCalendarListPage } from './list'; @NgModule({ diff --git a/src/addon/calendar/pages/settings/settings.module.ts b/src/addon/calendar/pages/settings/settings.module.ts index 93f443bd0..8b9fbaa23 100644 --- a/src/addon/calendar/pages/settings/settings.module.ts +++ b/src/addon/calendar/pages/settings/settings.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { AddonCalendarSettingsPage } from './settings'; -import { CorePipesModule } from '@pipes'; +import { CorePipesModule } from '@pipes/pipes.module'; @NgModule({ declarations: [ diff --git a/src/addon/files/pages/list/list.module.ts b/src/addon/files/pages/list/list.module.ts index f819f5512..94fd35db4 100644 --- a/src/addon/files/pages/list/list.module.ts +++ b/src/addon/files/pages/list/list.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; import { AddonFilesListPage } from './list'; @NgModule({ diff --git a/src/addon/messages/components/components.module.ts b/src/addon/messages/components/components.module.ts index 270ed0e3a..929d642f4 100644 --- a/src/addon/messages/components/components.module.ts +++ b/src/addon/messages/components/components.module.ts @@ -17,8 +17,8 @@ import { CommonModule } from '@angular/common'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; -import { CorePipesModule } from '@pipes'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; import { AddonMessagesDiscussionsComponent } from '../components/discussions/discussions'; import { AddonMessagesContactsComponent } from '../components/contacts/contacts'; diff --git a/src/addon/messages/pages/discussion/discussion.module.ts b/src/addon/messages/pages/discussion/discussion.module.ts index e0f82d74a..edf7ef9cb 100644 --- a/src/addon/messages/pages/discussion/discussion.module.ts +++ b/src/addon/messages/pages/discussion/discussion.module.ts @@ -17,8 +17,8 @@ import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { AddonMessagesDiscussionPage } from './discussion'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; -import { CorePipesModule } from '@pipes'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; @NgModule({ declarations: [ diff --git a/src/addon/messages/pages/settings/settings.module.ts b/src/addon/messages/pages/settings/settings.module.ts index a20e5f382..6e9071710 100644 --- a/src/addon/messages/pages/settings/settings.module.ts +++ b/src/addon/messages/pages/settings/settings.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { AddonMessagesSettingsPage } from './settings'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; import { AddonMessagesComponentsModule } from '../../components/components.module'; @NgModule({ diff --git a/src/addon/messages/providers/sync.ts b/src/addon/messages/providers/sync.ts index 45dcfbb24..6e2dfcbfe 100644 --- a/src/addon/messages/providers/sync.ts +++ b/src/addon/messages/providers/sync.ts @@ -21,6 +21,7 @@ import { AddonMessagesOfflineProvider } from './messages-offline'; import { AddonMessagesProvider } from './messages'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreEventsProvider } from '@providers/events'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { TranslateService } from '@ngx-translate/core'; import { CoreSyncProvider } from '@providers/sync'; @@ -37,8 +38,8 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { protected appProvider: CoreAppProvider, private messagesOffline: AddonMessagesOfflineProvider, private eventsProvider: CoreEventsProvider, private messagesProvider: AddonMessagesProvider, private userProvider: CoreUserProvider, private translate: TranslateService, private utils: CoreUtilsProvider, - syncProvider: CoreSyncProvider) { - super('AddonMessagesSync', sitesProvider, loggerProvider, appProvider, syncProvider); + syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider) { + super('AddonMessagesSync', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils); } /** diff --git a/src/addon/mod/book/components/components.module.ts b/src/addon/mod/book/components/components.module.ts index cf175a12f..b3db11442 100644 --- a/src/addon/mod/book/components/components.module.ts +++ b/src/addon/mod/book/components/components.module.ts @@ -17,7 +17,7 @@ import { CommonModule } from '@angular/common'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { AddonModBookIndexComponent } from './index/index'; import { AddonModBookTocPopoverComponent } from './toc-popover/toc-popover'; diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts index 8e2b4bb92..1183a04e4 100644 --- a/src/addon/mod/book/components/index/index.ts +++ b/src/addon/mod/book/components/index/index.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, Optional } from '@angular/core'; -import { NavParams, NavController, Content, PopoverController } from 'ionic-angular'; +import { Content, PopoverController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; diff --git a/src/addon/mod/book/pages/index/index.module.ts b/src/addon/mod/book/pages/index/index.module.ts index bce50baa7..14d2d6a29 100644 --- a/src/addon/mod/book/pages/index/index.module.ts +++ b/src/addon/mod/book/pages/index/index.module.ts @@ -15,7 +15,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; import { AddonModBookComponentsModule } from '../../components/components.module'; import { AddonModBookIndexPage } from './index'; diff --git a/src/addon/mod/book/providers/book.ts b/src/addon/mod/book/providers/book.ts index b6d36e194..808a77128 100644 --- a/src/addon/mod/book/providers/book.ts +++ b/src/addon/mod/book/providers/book.ts @@ -288,7 +288,7 @@ export class AddonModBookProvider { return []; } - return JSON.parse(contents[0].content); + return this.textUtils.parseJSON(contents[0].content, []); } /** diff --git a/src/addon/mod/book/providers/module-handler.ts b/src/addon/mod/book/providers/module-handler.ts index d88e95a24..e440fcf33 100644 --- a/src/addon/mod/book/providers/module-handler.ts +++ b/src/addon/mod/book/providers/module-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController, NavOptions } from 'ionic-angular'; import { AddonModBookProvider } from './book'; import { AddonModBookIndexComponent } from '../components/index/index'; @@ -60,12 +60,14 @@ export class AddonModBookModuleHandler implements CoreCourseModuleHandler { /** * Get the component to render the module. This is needed to support singleactivity course format. * The component returned must implement CoreCourseModuleMainComponent. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course object. * @param {any} module The module object. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getMainComponent(course: any, module: any): any { + getMainComponent(injector: Injector, course: any, module: any): any | Promise { return AddonModBookIndexComponent; } } diff --git a/src/addon/mod/book/providers/prefetch-handler.ts b/src/addon/mod/book/providers/prefetch-handler.ts index ca99a83fc..17aa4b636 100644 --- a/src/addon/mod/book/providers/prefetch-handler.ts +++ b/src/addon/mod/book/providers/prefetch-handler.ts @@ -78,22 +78,6 @@ export class AddonModBookPrefetchHandler extends CoreCourseModulePrefetchHandler return this.bookProvider.invalidateContent(moduleId, courseId); } - /** - * Invalidate WS calls needed to determine module status. - * - * @param {any} module Module. - * @param {number} courseId Course ID the module belongs to. - * @return {Promise} Promise resolved when invalidated. - */ - invalidateModule(module: any, courseId: number): Promise { - const promises = []; - - promises.push(this.bookProvider.invalidateBookData(courseId)); - promises.push(this.courseProvider.invalidateModule(module.id)); - - return Promise.all(promises); - } - /** * Whether or not the handler is enabled on a site level. * diff --git a/src/addon/mod/label/providers/module-handler.ts b/src/addon/mod/label/providers/module-handler.ts index 90cfdd780..06ab1e7f7 100644 --- a/src/addon/mod/label/providers/module-handler.ts +++ b/src/addon/mod/label/providers/module-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; /** @@ -58,12 +58,14 @@ export class AddonModLabelModuleHandler implements CoreCourseModuleHandler { /** * Get the component to render the module. This is needed to support singleactivity course format. * The component returned must implement CoreCourseModuleMainComponent. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course object. * @param {any} module The module object. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getMainComponent(course: any, module: any): any { + getMainComponent(injector: Injector, course: any, module: any): any | Promise { // There's no need to implement this because label cannot be used in singleactivity course format. } } diff --git a/src/addon/pushnotifications/providers/pushnotifications.ts b/src/addon/pushnotifications/providers/pushnotifications.ts index 75bbee3d4..9482de788 100644 --- a/src/addon/pushnotifications/providers/pushnotifications.ts +++ b/src/addon/pushnotifications/providers/pushnotifications.ts @@ -25,7 +25,7 @@ import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreConfigProvider } from '@providers/config'; -import { CoreConfigConstants } from '.././../../configconstants'; +import { CoreConfigConstants } from '../../../configconstants'; /** * Service to handle push notifications. diff --git a/src/addon/userprofilefield/checkbox/providers/handler.ts b/src/addon/userprofilefield/checkbox/providers/handler.ts index a3dbb51e3..4383da420 100644 --- a/src/addon/userprofilefield/checkbox/providers/handler.ts +++ b/src/addon/userprofilefield/checkbox/providers/handler.ts @@ -12,9 +12,8 @@ // 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 { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from - '../../../../core/user/providers/user-profile-field-delegate'; +import { Injectable, Injector } from '@angular/core'; +import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldCheckboxComponent } from '../component/checkbox'; /** @@ -60,10 +59,12 @@ export class AddonUserProfileFieldCheckboxHandler implements CoreUserProfileFiel /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonUserProfileFieldCheckboxComponent; } } diff --git a/src/addon/userprofilefield/datetime/datetime.module.ts b/src/addon/userprofilefield/datetime/datetime.module.ts index 9814fc7e5..dd2b3f85d 100644 --- a/src/addon/userprofilefield/datetime/datetime.module.ts +++ b/src/addon/userprofilefield/datetime/datetime.module.ts @@ -19,7 +19,7 @@ import { AddonUserProfileFieldDatetimeHandler } from './providers/handler'; import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldDatetimeComponent } from './component/datetime'; import { CoreComponentsModule } from '@components/components.module'; -import { CorePipesModule } from '@pipes'; +import { CorePipesModule } from '@pipes/pipes.module'; @NgModule({ declarations: [ diff --git a/src/addon/userprofilefield/datetime/providers/handler.ts b/src/addon/userprofilefield/datetime/providers/handler.ts index d8135b94d..2f2ae2513 100644 --- a/src/addon/userprofilefield/datetime/providers/handler.ts +++ b/src/addon/userprofilefield/datetime/providers/handler.ts @@ -12,9 +12,8 @@ // 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 { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from - '../../../../core/user/providers/user-profile-field-delegate'; +import { Injectable, Injector } from '@angular/core'; +import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldDatetimeComponent } from '../component/datetime'; /** @@ -62,10 +61,12 @@ export class AddonUserProfileFieldDatetimeHandler implements CoreUserProfileFiel /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonUserProfileFieldDatetimeComponent; } } diff --git a/src/addon/userprofilefield/menu/menu.module.ts b/src/addon/userprofilefield/menu/menu.module.ts index 0329b2c78..df81f32b5 100644 --- a/src/addon/userprofilefield/menu/menu.module.ts +++ b/src/addon/userprofilefield/menu/menu.module.ts @@ -19,7 +19,7 @@ import { AddonUserProfileFieldMenuHandler } from './providers/handler'; import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldMenuComponent } from './component/menu'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/addon/userprofilefield/menu/providers/handler.ts b/src/addon/userprofilefield/menu/providers/handler.ts index 931cef566..89d536561 100644 --- a/src/addon/userprofilefield/menu/providers/handler.ts +++ b/src/addon/userprofilefield/menu/providers/handler.ts @@ -12,9 +12,8 @@ // 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 { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from - '../../../../core/user/providers/user-profile-field-delegate'; +import { Injectable, Injector } from '@angular/core'; +import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldMenuComponent } from '../component/menu'; /** @@ -60,10 +59,12 @@ export class AddonUserProfileFieldMenuHandler implements CoreUserProfileFieldHan /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonUserProfileFieldMenuComponent; } } diff --git a/src/addon/userprofilefield/text/providers/handler.ts b/src/addon/userprofilefield/text/providers/handler.ts index 1953660ce..5c4ce7d9f 100644 --- a/src/addon/userprofilefield/text/providers/handler.ts +++ b/src/addon/userprofilefield/text/providers/handler.ts @@ -12,9 +12,8 @@ // 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 { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from - '../../../../core/user/providers/user-profile-field-delegate'; +import { Injectable, Injector } from '@angular/core'; +import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldTextComponent } from '../component/text'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -57,10 +56,12 @@ export class AddonUserProfileFieldTextHandler implements CoreUserProfileFieldHan /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonUserProfileFieldTextComponent; } } diff --git a/src/addon/userprofilefield/text/text.module.ts b/src/addon/userprofilefield/text/text.module.ts index a0cf647a6..8bc58388f 100644 --- a/src/addon/userprofilefield/text/text.module.ts +++ b/src/addon/userprofilefield/text/text.module.ts @@ -19,7 +19,7 @@ import { AddonUserProfileFieldTextHandler } from './providers/handler'; import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldTextComponent } from './component/text'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/addon/userprofilefield/textarea/providers/handler.ts b/src/addon/userprofilefield/textarea/providers/handler.ts index 1c07057db..b2bffda4e 100644 --- a/src/addon/userprofilefield/textarea/providers/handler.ts +++ b/src/addon/userprofilefield/textarea/providers/handler.ts @@ -12,9 +12,8 @@ // 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 { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from - '../../../../core/user/providers/user-profile-field-delegate'; +import { Injectable, Injector } from '@angular/core'; +import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldTextareaComponent } from '../component/textarea'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -66,10 +65,12 @@ export class AddonUserProfileFieldTextareaHandler implements CoreUserProfileFiel /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonUserProfileFieldTextareaComponent; } } diff --git a/src/addon/userprofilefield/textarea/textarea.module.ts b/src/addon/userprofilefield/textarea/textarea.module.ts index 1c2ce58ef..1a307b685 100644 --- a/src/addon/userprofilefield/textarea/textarea.module.ts +++ b/src/addon/userprofilefield/textarea/textarea.module.ts @@ -19,7 +19,7 @@ import { AddonUserProfileFieldTextareaHandler } from './providers/handler'; import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldTextareaComponent } from './component/textarea'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7b0be5c01..406450a66 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -49,6 +49,7 @@ import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreUpdateManagerProvider } from '@providers/update-manager'; import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; import { CoreSyncProvider } from '@providers/sync'; +import { CoreFileHelperProvider } from '@providers/file-helper'; // Core modules. import { CoreComponentsModule } from '@components/components.module'; @@ -64,6 +65,8 @@ import { CoreContentLinksModule } from '@core/contentlinks/contentlinks.module'; import { CoreUserModule } from '@core/user/user.module'; import { CoreGradesModule } from '@core/grades/grades.module'; import { CoreSettingsModule } from '@core/settings/settings.module'; +import { CoreSitePluginsModule } from '@core/siteplugins/siteplugins.module'; +import { CoreCompileModule } from '@core/compile/compile.module'; // Addon modules. import { AddonCalendarModule } from '@addon/calendar/calendar.module'; @@ -79,6 +82,36 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { return new TranslateHttpLoader(http, './assets/lang/', '.json'); } +// List of providers. +export const CORE_PROVIDERS: any[] = [ + CoreLoggerProvider, + CoreDbProvider, + CoreAppProvider, + CoreConfigProvider, + CoreLangProvider, + CoreTextUtilsProvider, + CoreDomUtilsProvider, + CoreTimeUtilsProvider, + CoreUrlUtilsProvider, + CoreUtilsProvider, + CoreMimetypeUtilsProvider, + CoreInitDelegate, + CoreFileProvider, + CoreWSProvider, + CoreEventsProvider, + CoreSitesFactoryProvider, + CoreSitesProvider, + CoreLocalNotificationsProvider, + CoreGroupsProvider, + CoreCronDelegate, + CoreFileSessionProvider, + CoreFilepoolProvider, + CoreUpdateManagerProvider, + CorePluginFileDelegate, + CoreSyncProvider, + CoreFileHelperProvider +]; + @NgModule({ declarations: [ MoodleMobileApp @@ -111,6 +144,8 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { CoreUserModule, CoreGradesModule, CoreSettingsModule, + CoreSitePluginsModule, + CoreCompileModule, AddonCalendarModule, AddonUserProfileFieldModule, AddonFilesModule, @@ -123,38 +158,13 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { entryComponents: [ MoodleMobileApp ], - providers: [ + providers: CORE_PROVIDERS.concat([ { provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true, - }, - CoreLoggerProvider, - CoreDbProvider, - CoreAppProvider, - CoreConfigProvider, - CoreLangProvider, - CoreTextUtilsProvider, - CoreDomUtilsProvider, - CoreTimeUtilsProvider, - CoreUrlUtilsProvider, - CoreUtilsProvider, - CoreMimetypeUtilsProvider, - CoreInitDelegate, - CoreFileProvider, - CoreWSProvider, - CoreEventsProvider, - CoreSitesFactoryProvider, - CoreSitesProvider, - CoreLocalNotificationsProvider, - CoreGroupsProvider, - CoreCronDelegate, - CoreFileSessionProvider, - CoreFilepoolProvider, - CoreUpdateManagerProvider, - CorePluginFileDelegate, - CoreSyncProvider - ] + } + ]) }) export class AppModule { constructor(platform: Platform, initDelegate: CoreInitDelegate, updateManager: CoreUpdateManagerProvider, diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts index e1e70a2c5..654b3bba8 100644 --- a/src/classes/base-sync.ts +++ b/src/classes/base-sync.ts @@ -16,6 +16,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreAppProvider } from '@providers/app'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; /** * Base class to create sync providers. It provides some common functions. @@ -44,7 +45,8 @@ export class CoreSyncBaseProvider { protected syncPromises: { [siteId: string]: { [uniqueId: string]: Promise } } = {}; constructor(component: string, protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, - protected appProvider: CoreAppProvider, protected syncProvider: CoreSyncProvider) { + protected appProvider: CoreAppProvider, protected syncProvider: CoreSyncProvider, + protected textUtils: CoreTextUtilsProvider) { this.logger = this.loggerProvider.getInstance(component); this.component = component; } @@ -115,11 +117,7 @@ export class CoreSyncBaseProvider { */ getSyncWarnings(id: string | number, siteId?: string): Promise { return this.syncProvider.getSyncRecord(this.component, id, siteId).then((entry) => { - try { - return JSON.parse(entry.warnings); - } catch (ex) { - return []; - } + return this.textUtils.parseJSON(entry.warnings, []); }).catch(() => { return []; }); diff --git a/src/classes/delegate.ts b/src/classes/delegate.ts index 00e1bf791..7f19b8c00 100644 --- a/src/classes/delegate.ts +++ b/src/classes/delegate.ts @@ -91,7 +91,7 @@ export class CoreDelegate { // Update handlers on this cases. eventsProvider.on(CoreEventsProvider.LOGIN, this.updateHandlers.bind(this)); eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.updateHandlers.bind(this)); - eventsProvider.on(CoreEventsProvider.REMOTE_ADDONS_LOADED, this.updateHandlers.bind(this)); + eventsProvider.on(CoreEventsProvider.SITE_PLUGINS_LOADED, this.updateHandlers.bind(this)); } } diff --git a/src/classes/site.ts b/src/classes/site.ts index 3c3480f25..682561a4e 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -734,7 +734,7 @@ export class CoreSite { const expires = (entry.expirationTime - now) / 1000; this.logger.info(`Cached element found, id: ${id} expires in ${expires} seconds`); - return JSON.parse(entry.data); + return this.textUtils.parseJSON(entry.data, {}); } return Promise.reject(null); diff --git a/src/components/components.module.ts b/src/components/components.module.ts index 0c4a8b7de..ff4ae9592 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -15,8 +15,8 @@ import { NgModule } from '@angular/core'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreDirectivesModule } from '@directives'; -import { CorePipesModule } from '@pipes'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; import { CoreLoadingComponent } from './loading/loading'; import { CoreMarkRequiredComponent } from './mark-required/mark-required'; import { CoreInputErrorsComponent } from './input-errors/input-errors'; diff --git a/src/components/dynamic-component/dynamic-component.ts b/src/components/dynamic-component/dynamic-component.ts index f2a045043..c41afc529 100644 --- a/src/components/dynamic-component/dynamic-component.ts +++ b/src/components/dynamic-component/dynamic-component.ts @@ -13,9 +13,10 @@ // limitations under the License. import { - Component, Input, ViewChild, OnInit, OnChanges, DoCheck, ViewContainerRef, ComponentFactoryResolver, - KeyValueDiffers, SimpleChange + Component, Input, ViewChild, OnInit, OnChanges, DoCheck, ViewContainerRef, ComponentFactoryResolver, ComponentRef, + KeyValueDiffers, SimpleChange, ChangeDetectorRef, Optional, ElementRef } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { CoreLoggerProvider } from '@providers/logger'; /** @@ -39,6 +40,10 @@ import { CoreLoggerProvider } from '@providers/logger'; * * Please notice that the component that you pass needs to be declared in entryComponents of the module to be created dynamically. * + * Alternatively, you can also supply a ComponentRef instead of the class of the component. In this case, the component won't + * be instantiated because it already is, it will be attached to the view and the right data will be passed to it. + * Passing ComponentRef is meant for site plugins, so we'll inject a NavController instance to the component. + * * The contents of this component will be displayed if no component is supplied or it cannot be created. In the example above, * if no component is supplied then the template will show the message "Cannot render the data.". */ @@ -62,7 +67,8 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck { protected logger: any; protected differ: any; // To detect changes in the data input. - constructor(logger: CoreLoggerProvider, private factoryResolver: ComponentFactoryResolver, differs: KeyValueDiffers) { + constructor(logger: CoreLoggerProvider, protected factoryResolver: ComponentFactoryResolver, differs: KeyValueDiffers, + @Optional() protected navCtrl: NavController, protected cdr: ChangeDetectorRef, protected element: ElementRef) { this.logger = logger.getInstance('CoreDynamicComponent'); this.differ = differs.find([]).create(); } @@ -128,21 +134,32 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck { return true; } - try { - // Create the component and add it to the container. - const factory = this.factoryResolver.resolveComponentFactory(this.component), - componentRef = this.container.createComponent(factory); + if (this.component instanceof ComponentRef) { + // A ComponentRef was supplied instead of the component class. Add it to the view. + this.container.insert(this.component.hostView); + this.instance = this.component.instance; - this.instance = componentRef.instance; + // This feature is usually meant for site plugins. Inject some properties. + this.instance['ChangeDetectorRef'] = this.cdr; + this.instance['NavController'] = this.navCtrl; + this.instance['componentContainer'] = this.element.nativeElement; + } else { + try { + // Create the component and add it to the container. + const factory = this.factoryResolver.resolveComponentFactory(this.component), + componentRef = this.container.createComponent(factory); - this.setInputData(); + this.instance = componentRef.instance; + } catch (ex) { + this.logger.error('Error creating component', ex); - return true; - } catch (ex) { - this.logger.error('Error creating component', ex); - - return false; + return false; + } } + + this.setInputData(); + + return true; } /** diff --git a/src/components/file/file.ts b/src/components/file/file.ts index 20023d454..7ed4d61c6 100644 --- a/src/components/file/file.ts +++ b/src/components/file/file.ts @@ -13,11 +13,10 @@ // limitations under the License. import { Component, Input, Output, OnInit, OnDestroy, EventEmitter } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; -import { CoreFileProvider } from '@providers/file'; import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreFileHelperProvider } from '@providers/file-helper'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; @@ -36,7 +35,6 @@ export class CoreFileComponent implements OnInit, OnDestroy { @Input() file: any; // The file. Must have a property 'filename' and a 'fileurl' or 'url' @Input() component?: string; // Component the file belongs to. @Input() componentId?: string | number; // Component ID. - @Input() timemodified?: number; // If set, the value will be used to check if the file is outdated. @Input() canDelete?: boolean | string; // Whether file can be deleted. @Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded. // Use it for files that you cannot determine if they're outdated or not. @@ -52,12 +50,14 @@ export class CoreFileComponent implements OnInit, OnDestroy { protected fileUrl: string; protected siteId: string; protected fileSize: number; + protected state: string; + protected timemodified: number; protected observer; - constructor(private translate: TranslateService, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, - private domUtils: CoreDomUtilsProvider, private filepoolProvider: CoreFilepoolProvider, - private fileProvider: CoreFileProvider, private appProvider: CoreAppProvider, - private mimeUtils: CoreMimetypeUtilsProvider, private eventsProvider: CoreEventsProvider) { + constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider, + private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider, + private fileHelper: CoreFileHelperProvider, private mimeUtils: CoreMimetypeUtilsProvider, + private eventsProvider: CoreEventsProvider) { this.onDelete = new EventEmitter(); } @@ -68,9 +68,9 @@ export class CoreFileComponent implements OnInit, OnDestroy { this.canDelete = this.utils.isTrueOrOne(this.canDelete); this.alwaysDownload = this.utils.isTrueOrOne(this.alwaysDownload); this.canDownload = this.utils.isTrueOrOne(this.canDownload); - this.timemodified = this.timemodified || 0; - this.fileUrl = this.file.fileurl || this.file.url; + this.fileUrl = this.fileHelper.getFileUrl(this.file); + this.timemodified = this.fileHelper.getFileTimemodified(this.file); this.siteId = this.sitesProvider.getCurrentSiteId(); this.fileSize = this.file.filesize; this.fileName = this.file.filename; @@ -102,6 +102,7 @@ export class CoreFileComponent implements OnInit, OnDestroy { return this.filepoolProvider.getFileStateByUrl(this.siteId, this.fileUrl, this.timemodified).then((state) => { const canDownload = this.sitesProvider.getCurrentSite().canDownloadFiles(); + this.state = state; this.isDownloaded = state === CoreConstants.DOWNLOADED || state === CoreConstants.OUTDATED; this.isDownloading = canDownload && state === CoreConstants.DOWNLOADING; this.showDownload = canDownload && (state === CoreConstants.NOT_DOWNLOADED || state === CoreConstants.OUTDATED || @@ -109,123 +110,19 @@ export class CoreFileComponent implements OnInit, OnDestroy { }); } - /** - * Download the file. - * - * @return {Promise} Promise resolved when file is downloaded. - */ - protected downloadFile(): Promise { - if (!this.sitesProvider.getCurrentSite().canDownloadFiles()) { - this.domUtils.showErrorModal('core.cannotdownloadfiles', true); - - return Promise.reject(null); - } - - this.isDownloading = true; - - return this.filepoolProvider.downloadUrl(this.siteId, this.fileUrl, false, this.component, this.componentId, - this.timemodified, undefined, undefined, this.file).catch(() => { - - // Call calculateState to make sure we have the right state. - return this.calculateState().then(() => { - if (this.isDownloaded) { - return this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.fileUrl); - } else { - return Promise.reject(null); - } - }); - }); - } - /** * Convenience function to open a file, downloading it if needed. * * @return {Promise} Promise resolved when file is opened. */ protected openFile(): Promise { - const fixedUrl = this.sitesProvider.getCurrentSite().fixPluginfileURL(this.fileUrl); - let promise; - - if (this.fileProvider.isAvailable()) { - promise = Promise.resolve().then(() => { - // The file system is available. - const isWifi = !this.appProvider.isNetworkAccessLimited(), - isOnline = this.appProvider.isOnline(); - - if (this.isDownloaded && !this.showDownload) { - // File is downloaded, get the local file URL. - return this.filepoolProvider.getUrlByUrl(this.siteId, this.fileUrl, - this.component, this.componentId, this.timemodified, false, false, this.file); - } else { - if (!isOnline && !this.isDownloaded) { - // Not downloaded and user is offline, reject. - return Promise.reject(this.translate.instant('core.networkerrormsg')); - } - - const isDownloading = this.isDownloading; - this.isDownloading = true; // This check could take a while, show spinner. - - return this.filepoolProvider.shouldDownloadBeforeOpen(fixedUrl, this.fileSize).then(() => { - if (isDownloading) { - // It's already downloading, stop. - return; - } - - // Download and then return the local URL. - return this.downloadFile(); - }, () => { - // Start the download if in wifi, but return the URL right away so the file is opened. - if (isWifi && isOnline) { - this.downloadFile(); - } - - if (isDownloading || !this.isDownloaded || isOnline) { - // Not downloaded or outdated and online, return the online URL. - return fixedUrl; - } else { - // Outdated but offline, so we return the local URL. - return this.filepoolProvider.getUrlByUrl(this.siteId, this.fileUrl, - this.component, this.componentId, this.timemodified, false, false, this.file); - } - }); - } - }); - } else { - // Use the online URL. - promise = Promise.resolve(fixedUrl); - } - - return promise.then((url) => { - if (!url) { - return; - } - - if (url.indexOf('http') === 0) { - return this.utils.openOnlineFile(url).catch((error) => { - // Error opening the file, some apps don't allow opening online files. - if (!this.fileProvider.isAvailable()) { - return Promise.reject(error); - } else if (this.isDownloading) { - return Promise.reject(this.translate.instant('core.erroropenfiledownloading')); - } - - let subPromise; - - if (status === CoreConstants.NOT_DOWNLOADED) { - // File is not downloaded, download and then return the local URL. - subPromise = this.downloadFile(); - } else { - // File is outdated and can't be opened in online, return the local URL. - subPromise = this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.fileUrl); - } - - return subPromise.then((url) => { - return this.utils.openFile(url); - }); - }); - } else { - return this.utils.openFile(url); + return this.fileHelper.downloadAndOpenFile(this.file, this.component, this.componentId, this.state, (event) => { + if (event && event.calculating) { + // The process is calculating some data required for the download, show the spinner. + this.isDownloading = true; } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); }); } diff --git a/src/components/split-view/split-view.ts b/src/components/split-view/split-view.ts index 0ab53cca2..7d5a936b7 100644 --- a/src/components/split-view/split-view.ts +++ b/src/components/split-view/split-view.ts @@ -14,7 +14,7 @@ // Code based on https://github.com/martinpritchardelevate/ionic-split-pane-demo -import { Component, ViewChild, Input, ElementRef, OnInit } from '@angular/core'; +import { Component, ViewChild, Input, ElementRef, OnInit, Optional } from '@angular/core'; import { NavController, Nav } from 'ionic-angular'; /** @@ -54,7 +54,7 @@ export class CoreSplitViewComponent implements OnInit { // Empty placeholder for the 'detail' page. detailPage: any = null; - constructor(private masterNav: NavController, element: ElementRef) { + constructor(@Optional() private masterNav: NavController, element: ElementRef) { this.element = element.nativeElement; } diff --git a/src/core/compile/compile.module.ts b/src/core/compile/compile.module.ts new file mode 100644 index 000000000..610277494 --- /dev/null +++ b/src/core/compile/compile.module.ts @@ -0,0 +1,27 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreCompileProvider } from './providers/compile'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + CoreCompileProvider + ] +}) +export class CoreCompileModule { } diff --git a/src/core/compile/components/compile-html/compile-html.module.ts b/src/core/compile/components/compile-html/compile-html.module.ts new file mode 100644 index 000000000..84616b2fa --- /dev/null +++ b/src/core/compile/components/compile-html/compile-html.module.ts @@ -0,0 +1,30 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreCompileHtmlComponent } from './compile-html'; + +@NgModule({ + declarations: [ + CoreCompileHtmlComponent + ], + imports: [ + CommonModule + ], + exports: [ + CoreCompileHtmlComponent + ] +}) +export class CoreCompileHtmlComponentModule {} diff --git a/src/core/compile/components/compile-html/compile-html.ts b/src/core/compile/components/compile-html/compile-html.ts new file mode 100644 index 000000000..fcbd23171 --- /dev/null +++ b/src/core/compile/components/compile-html/compile-html.ts @@ -0,0 +1,122 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnInit, OnChanges, OnDestroy, ViewContainerRef, ViewChild, ComponentRef, SimpleChange, ChangeDetectorRef, + ElementRef, Optional +} from '@angular/core'; +import { NavController } from 'ionic-angular'; +import { CoreCompileProvider } from '../../providers/compile'; +import { BehaviorSubject } from 'rxjs'; + +/** + * This component has a behaviour similar to $compile for AngularJS. Given an HTML code, it will compile it so all its + * components and directives are instantiated. + * + * IMPORTANT: Use this component only if it is a must. It will create and compile a new component and module everytime this + * component is used, so it can slow down the app. + * + * This component has its own module to prevent circular dependencies. If you want to use it, + * you need to import CoreCompileHtmlComponentModule. + * + * You can provide some Javascript code (as text) to be executed inside the component. The context of the javascript code (this) + * will be the component instance created to compile the template. This means your javascript code can interact with the template. + * The component instance will have most of the providers so you can use them in the javascript code. E.g. if you want to use + * CoreAppProvider, you can do it with "this.CoreAppProvider". + */ +@Component({ + selector: 'core-compile-html', + template: '' +}) +export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { + @Input() text: string; // The HTML text to display. + @Input() javascript: string; // The Javascript to execute in the component. + @Input() jsData; // Data to pass to the fake component. + + // Get the container where to put the content. + @ViewChild('dynamicComponent', { read: ViewContainerRef }) container: ViewContainerRef; + + protected componentRef: ComponentRef; + protected element; + componentObservable: BehaviorSubject; // An observable to notify observers when the component is instantiated. + + constructor(protected compileProvider: CoreCompileProvider, protected cdr: ChangeDetectorRef, element: ElementRef, + @Optional() protected navCtrl: NavController) { + this.element = element.nativeElement; + this.componentObservable = new BehaviorSubject(null); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: { [name: string]: SimpleChange }): void { + if ((changes.text || changes.javascript) && this.text) { + // Create a new component and a new module. + this.compileProvider.createAndCompileComponent(this.text, this.getComponentClass()).then((factory) => { + // Destroy previous components. + this.componentRef && this.componentRef.destroy(); + + if (factory) { + // Create the component. + this.componentRef = this.container.createComponent(factory); + this.componentObservable.next(this.componentRef.instance); + } + }); + } + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.componentRef && this.componentRef.destroy(); + } + + /** + * Get a class that defines the dynamic component. + * + * @return {any} The component class. + */ + protected getComponentClass(): any { + // tslint:disable: no-this-assignment + const compileInstance = this; + + // Create the component, using the text as the template. + return class CoreCompileHtmlFakeComponent implements OnInit { + constructor() { + // If there is some javascript to run, prepare the instance. + if (compileInstance.javascript) { + compileInstance.compileProvider.injectLibraries(this); + + // Add some more components and classes. + this['ChangeDetectorRef'] = compileInstance.cdr; + this['NavController'] = compileInstance.navCtrl; + this['componentContainer'] = compileInstance.element; + } + + // Add the data passed to the component. + for (const name in compileInstance.jsData) { + this[name] = compileInstance.jsData[name]; + } + } + + ngOnInit(): void { + // If there is some javascript to run, do it now. + if (compileInstance.javascript) { + compileInstance.compileProvider.executeJavascript(this, compileInstance.javascript); + } + } + }; + } +} diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts new file mode 100644 index 000000000..d864d999f --- /dev/null +++ b/src/core/compile/providers/compile.ts @@ -0,0 +1,224 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector, Component, NgModule, Compiler, ComponentFactory, ComponentRef, NgModuleRef } from '@angular/core'; +import { + Platform, ActionSheetController, AlertController, LoadingController, ModalController, PopoverController, ToastController, + IonicModule +} from 'ionic-angular'; +import { TranslateService, TranslateModule } from '@ngx-translate/core'; +import { CoreLoggerProvider } from '@providers/logger'; + +// Import core providers. +import { CORE_PROVIDERS } from '@app/app.module'; +import { CORE_CONTENTLINKS_PROVIDERS } from '@core/contentlinks/contentlinks.module'; +import { CORE_COURSE_PROVIDERS } from '@core/course/course.module'; +import { CORE_COURSES_PROVIDERS } from '@core/courses/courses.module'; +import { CORE_FILEUPLOADER_PROVIDERS } from '@core/fileuploader/fileuploader.module'; +import { CORE_GRADES_PROVIDERS } from '@core/grades/grades.module'; +import { CORE_LOGIN_PROVIDERS } from '@core/login/login.module'; +import { CORE_MAINMENU_PROVIDERS } from '@core/mainmenu/mainmenu.module'; +import { CORE_SHAREDFILES_PROVIDERS } from '@core/sharedfiles/sharedfiles.module'; +import { CORE_SITEHOME_PROVIDERS } from '@core/sitehome/sitehome.module'; +import { CORE_USER_PROVIDERS } from '@core/user/user.module'; +import { IONIC_NATIVE_PROVIDERS } from '@core/emulator/emulator.module'; + +// Import only this provider to prevent circular dependencies. +import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins'; + +// Import other libraries and providers. +import { DomSanitizer } from '@angular/platform-browser'; +import { FormBuilder, Validators } from '@angular/forms'; +import { Http } from '@angular/http'; +import { HttpClient } from '@angular/common/http'; +import { CoreConfigConstants } from '../../../configconstants'; +import { CoreConstants } from '@core/constants'; +import * as moment from 'moment'; +import { Md5 } from 'ts-md5/dist/md5'; + +// Import core classes that can be useful for site plugins. +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCache } from '@classes/cache'; +import { CoreDelegate } from '@classes/delegate'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksModuleGradeHandler } from '@core/contentlinks/classes/module-grade-handler'; +import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; +import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; + +// Import all modules that define components, directives and pipes. +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { CoreCourseComponentsModule } from '@core/course/components/components.module'; +import { CoreCourseDirectivesModule } from '@core/course/directives/directives.module'; +import { CoreCoursesComponentsModule } from '@core/courses/components/components.module'; +import { CoreSitePluginsDirectivesModule } from '@core/siteplugins/directives/directives.module'; +import { CoreSiteHomeComponentsModule } from '@core/sitehome/components/components.module'; +import { CoreUserComponentsModule } from '@core/user/components/components.module'; + +// Import some components listed in entryComponents so they can be injected dynamically. +import { CoreCourseUnsupportedModuleComponent } from '@core/course/components/unsupported-module/unsupported-module'; +import { CoreCourseFormatSingleActivityComponent } from '@core/course/formats/singleactivity/components/singleactivity'; +import { CoreSitePluginsModuleIndexComponent } from '@core/siteplugins/components/module-index/module-index'; +import { CoreSitePluginsCourseOptionComponent } from '@core/siteplugins/components/course-option/course-option'; +import { CoreSitePluginsCourseFormatComponent } from '@core/siteplugins/components/course-format/course-format'; + +/** + * Service to provide functionalities regarding compiling dynamic HTML and Javascript. + */ +@Injectable() +export class CoreCompileProvider { + + protected logger; + + // Other Ionic/Angular providers that don't depend on where they are injected. + protected OTHER_PROVIDERS = [ + TranslateService, Http, HttpClient, Platform, DomSanitizer, ActionSheetController, AlertController, LoadingController, + ModalController, PopoverController, ToastController, FormBuilder + ]; + + // List of imports for dynamic module. Since the template can have any component we need to import all core components modules. + protected IMPORTS = [ + IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule, + CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule, + CoreCourseDirectivesModule, CoreSitePluginsDirectivesModule + ]; + + constructor(protected injector: Injector, logger: CoreLoggerProvider, protected compiler: Compiler) { + this.logger = logger.getInstance('CoreCompileProvider'); + } + + /** + * Create and compile a dynamic component. + * + * @param {string} template The template of the component. + * @param {any} componentClass The JS class of the component. + * @return {Promise>} Promise resolved with the factory to instantiate the component. + */ + createAndCompileComponent(template: string, componentClass: any): Promise> { + // Create the component using the template and the class. + const component = Component({ + template: template + }) + (componentClass); + + // Now create the module containing the component. + const module = NgModule({imports: this.IMPORTS, declarations: [component]})(class {}); + + // Compile the module and the component. + return this.compiler.compileModuleAndAllComponentsAsync(module).then((factories) => { + // Search and return the factory of the component we just created. + for (const i in factories.componentFactories) { + const factory = factories.componentFactories[i]; + if (factory.componentType == component) { + return factory; + } + } + }); + } + + /** + * Eval some javascript using the context of the function. + * + * @param {string} javascript The javascript to eval. + * @return {any} Result of the eval. + */ + protected evalInContext(javascript: string): any { + // tslint:disable: no-eval + return eval(javascript); + } + + /** + * Execute some javascript code, using a certain instance as the context. + * + * @param {any} instance Instance to use as the context. In the JS code, "this" will be this instance. + * @param {string} javascript The javascript code to eval. + * @return {any} Result of the javascript execution. + */ + executeJavascript(instance: any, javascript: string): any { + try { + return this.evalInContext.call(instance, javascript); + } catch (ex) { + this.logger.error('Error evaluating javascript', ex); + } + } + + /** + * Inject all the core libraries in a certain object. + * + * @param {any} instance The instance where to inject the libraries. + */ + injectLibraries(instance: any): void { + const providers = ( CORE_PROVIDERS).concat(CORE_CONTENTLINKS_PROVIDERS).concat(CORE_COURSE_PROVIDERS) + .concat(CORE_COURSES_PROVIDERS).concat(CORE_FILEUPLOADER_PROVIDERS).concat(CORE_GRADES_PROVIDERS) + .concat(CORE_LOGIN_PROVIDERS).concat(CORE_MAINMENU_PROVIDERS).concat(CORE_SHAREDFILES_PROVIDERS) + .concat(CORE_SITEHOME_PROVIDERS).concat([CoreSitePluginsProvider]).concat(CORE_USER_PROVIDERS) + .concat(IONIC_NATIVE_PROVIDERS).concat(this.OTHER_PROVIDERS); + + // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance. + for (const i in providers) { + const providerDef = providers[i]; + if (typeof providerDef == 'function' && providerDef.name) { + try { + // Inject the provider to the instance. We use the class name as the property name. + instance[providerDef.name] = this.injector.get(providerDef); + } catch (ex) { + this.logger.warn('Error injecting provider', providerDef.name, ex); + } + } + } + + // Inject current service. + instance['CoreCompileProvider'] = this; + + // Add some final classes. + instance['injector'] = this.injector; + instance['Validators'] = Validators; + instance['CoreConfigConstants'] = CoreConfigConstants; + instance['CoreConstants'] = CoreConstants; + instance['moment'] = moment; + instance['Md5'] = Md5; + instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider; + instance['CoreCache'] = CoreCache; + instance['CoreDelegate'] = CoreDelegate; + instance['CoreContentLinksHandlerBase'] = CoreContentLinksHandlerBase; + instance['CoreContentLinksModuleGradeHandler'] = CoreContentLinksModuleGradeHandler; + instance['CoreContentLinksModuleIndexHandler'] = CoreContentLinksModuleIndexHandler; + instance['CoreCourseModulePrefetchHandlerBase'] = CoreCourseModulePrefetchHandlerBase; + instance['CoreCourseUnsupportedModuleComponent'] = CoreCourseUnsupportedModuleComponent; + instance['CoreCourseFormatSingleActivityComponent'] = CoreCourseFormatSingleActivityComponent; + instance['CoreSitePluginsModuleIndexComponent'] = CoreSitePluginsModuleIndexComponent; + instance['CoreSitePluginsCourseOptionComponent'] = CoreSitePluginsCourseOptionComponent; + instance['CoreSitePluginsCourseFormatComponent'] = CoreSitePluginsCourseFormatComponent; + } + + /** + * Instantiate a dynamic component. + * + * @param {string} template The template of the component. + * @param {any} componentClass The JS class of the component. + * @param {Injector} [injector] The injector to use. It's recommended to pass it so NavController and similar can be injected. + * @return {Promise>} Promise resolved with the component instance. + */ + instantiateDynamicComponent(template: string, componentClass: any, injector?: Injector): Promise> { + injector = injector || this.injector; + + return this.createAndCompileComponent(template, componentClass).then((factory) => { + if (factory) { + // Create and return the component. + return factory.create(injector, undefined, undefined, injector.get(NgModuleRef)); + } + }); + } +} diff --git a/src/core/contentlinks/classes/module-grade-handler.ts b/src/core/contentlinks/classes/module-grade-handler.ts index 36296e5b6..68c26f9b6 100644 --- a/src/core/contentlinks/classes/module-grade-handler.ts +++ b/src/core/contentlinks/classes/module-grade-handler.ts @@ -17,7 +17,7 @@ import { CoreContentLinksAction } from '../providers/delegate'; import { CoreContentLinksHandlerBase } from './base-handler'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreCourseHelperProvider } from '../../course/providers/helper'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; /** * Handler to handle URLs pointing to the grade of a module. diff --git a/src/core/contentlinks/classes/module-index-handler.ts b/src/core/contentlinks/classes/module-index-handler.ts index 32fb2007a..28413ed26 100644 --- a/src/core/contentlinks/classes/module-index-handler.ts +++ b/src/core/contentlinks/classes/module-index-handler.ts @@ -14,7 +14,7 @@ import { CoreContentLinksAction } from '../providers/delegate'; import { CoreContentLinksHandlerBase } from './base-handler'; -import { CoreCourseHelperProvider } from '../../course/providers/helper'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; /** * Handler to handle URLs pointing to the index of a module. diff --git a/src/core/contentlinks/contentlinks.module.ts b/src/core/contentlinks/contentlinks.module.ts index 05c3ea838..0abe605db 100644 --- a/src/core/contentlinks/contentlinks.module.ts +++ b/src/core/contentlinks/contentlinks.module.ts @@ -16,14 +16,17 @@ import { NgModule } from '@angular/core'; import { CoreContentLinksDelegate } from './providers/delegate'; import { CoreContentLinksHelperProvider } from './providers/helper'; +// List of providers. +export const CORE_CONTENTLINKS_PROVIDERS = [ + CoreContentLinksDelegate, + CoreContentLinksHelperProvider +]; + @NgModule({ declarations: [], imports: [ ], - providers: [ - CoreContentLinksDelegate, - CoreContentLinksHelperProvider - ], + providers: CORE_CONTENTLINKS_PROVIDERS, exports: [] }) export class CoreContentLinksModule {} diff --git a/src/core/contentlinks/pages/choose-site/choose-site.module.ts b/src/core/contentlinks/pages/choose-site/choose-site.module.ts index 10e7ffe90..881bde925 100644 --- a/src/core/contentlinks/pages/choose-site/choose-site.module.ts +++ b/src/core/contentlinks/pages/choose-site/choose-site.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { CoreContentLinksChooseSitePage } from './choose-site'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/contentlinks/providers/helper.ts b/src/core/contentlinks/providers/helper.ts index c160a86b1..2eb8be01f 100644 --- a/src/core/contentlinks/providers/helper.ts +++ b/src/core/contentlinks/providers/helper.ts @@ -23,10 +23,11 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; -import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreContentLinksDelegate, CoreContentLinksAction } from './delegate'; -import { CoreConstants } from '../../constants'; +import { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../../../configconstants'; +import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins'; /** * Service that provides some features regarding content links. @@ -38,7 +39,8 @@ export class CoreContentLinksHelperProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, private contentLinksDelegate: CoreContentLinksDelegate, private appProvider: CoreAppProvider, private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private translate: TranslateService, - private initDelegate: CoreInitDelegate, eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider) { + private initDelegate: CoreInitDelegate, eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, + private sitePluginsProvider: CoreSitePluginsProvider) { this.logger = logger.getInstance('CoreContentLinksHelperProvider'); // Listen for app launched URLs. If we receive one, check if it's a content link. @@ -144,7 +146,6 @@ export class CoreContentLinksHelperProvider { return this.sitesProvider.checkSite(siteUrl).then((result) => { // Site exists. We'll allow to add it. const ssoNeeded = this.loginHelper.isSSOLoginNeeded(result.code), - hasRemoteAddonsLoaded = false, pageName = 'CoreLoginCredentialsPage', pageParams = { siteUrl: result.siteUrl, @@ -152,7 +153,8 @@ export class CoreContentLinksHelperProvider { urlToOpen: url, siteConfig: result.config }; - let promise; + let promise, + hasSitePluginsLoaded = false; modal.dismiss(); // Dismiss modal so it doesn't collide with confirms. @@ -164,8 +166,8 @@ export class CoreContentLinksHelperProvider { const confirmMsg = this.translate.instant('core.contentlinks.confirmurlothersite'); promise = this.domUtils.showConfirm(confirmMsg).then(() => { if (!ssoNeeded) { - // @todo hasRemoteAddonsLoaded = $mmAddonManager.hasRemoteAddonsLoaded(); @todo - if (hasRemoteAddonsLoaded) { + hasSitePluginsLoaded = this.sitePluginsProvider.hasSitePluginsLoaded; + if (hasSitePluginsLoaded) { // Store the redirect since logout will restart the app. this.appProvider.storeRedirect(CoreConstants.NO_SITE_ID, pageName, pageParams); } @@ -181,7 +183,7 @@ export class CoreContentLinksHelperProvider { if (ssoNeeded) { this.loginHelper.confirmAndOpenBrowserForSSOLogin( result.siteUrl, result.code, result.service, result.config && result.config.launchurl); - } else if (!hasRemoteAddonsLoaded) { + } else if (!hasSitePluginsLoaded) { this.appProvider.getRootNavController().setRoot(pageName, pageParams); } }); diff --git a/src/core/course/classes/module-prefetch-handler.ts b/src/core/course/classes/module-prefetch-handler.ts index 7fde10f08..c078e1b6a 100644 --- a/src/core/course/classes/module-prefetch-handler.ts +++ b/src/core/course/classes/module-prefetch-handler.ts @@ -133,10 +133,11 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * * @param {any} module The module object returned by WS. * @param {number} courseId Course ID. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. * @return {Promise} Promise resolved when all content is downloaded. */ - download(module: any, courseId: number): Promise { - return this.downloadOrPrefetch(module, courseId, false); + download(module: any, courseId: number, dirPath?: string): Promise { + return this.downloadOrPrefetch(module, courseId, false, dirPath); } /** @@ -332,8 +333,8 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref } /** - * Invalidate WS calls needed to determine module status. It doesn't need to invalidate check updates. - * It should NOT invalidate files nor all the prefetched data. + * Invalidate WS calls needed to determine module status (usually, to check if module is downloadable). + * It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data. * * @param {any} module Module. * @param {number} courseId Course ID the module belongs to. @@ -409,10 +410,11 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * @param {any} module Module. * @param {number} courseId Course ID the module belongs to. * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. * @return {Promise} Promise resolved when done. */ - prefetch(module: any, courseId?: number, single?: boolean): Promise { - return this.downloadOrPrefetch(module, courseId, true); + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise { + return this.downloadOrPrefetch(module, courseId, true, dirPath); } /** diff --git a/src/core/course/components/components.module.ts b/src/core/course/components/components.module.ts index 021427bf3..a56f920d8 100644 --- a/src/core/course/components/components.module.ts +++ b/src/core/course/components/components.module.ts @@ -17,7 +17,7 @@ import { CommonModule } from '@angular/common'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreCourseFormatComponent } from './format/format'; import { CoreCourseModuleComponent } from './module/module'; import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index b4a2985a9..504f5d0a3 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -13,17 +13,17 @@ // limitations under the License. import { - Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter, ViewChildren, QueryList + Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter, ViewChildren, QueryList, Injector } from '@angular/core'; import { Content } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreCourseProvider } from '../../../course/providers/course'; -import { CoreCourseHelperProvider } from '../../../course/providers/helper'; -import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate'; -import { CoreCourseModulePrefetchDelegate } from '../../../course/providers/module-prefetch-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; /** @@ -69,7 +69,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { protected sectionStatusObserver; - constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, + constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, private injector: Injector, private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider, eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider, private content: Content, prefetchDelegate: CoreCourseModulePrefetchDelegate) { @@ -194,19 +194,29 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { protected getComponents(): void { if (this.course) { if (!this.courseFormatComponent) { - this.courseFormatComponent = this.cfDelegate.getCourseFormatComponent(this.course); + this.cfDelegate.getCourseFormatComponent(this.injector, this.course).then((component) => { + this.courseFormatComponent = component; + }); } if (!this.courseSummaryComponent) { - this.courseSummaryComponent = this.cfDelegate.getCourseSummaryComponent(this.course); + this.cfDelegate.getCourseSummaryComponent(this.injector, this.course).then((component) => { + this.courseSummaryComponent = component; + }); } if (!this.sectionSelectorComponent) { - this.sectionSelectorComponent = this.cfDelegate.getSectionSelectorComponent(this.course); + this.cfDelegate.getSectionSelectorComponent(this.injector, this.course).then((component) => { + this.sectionSelectorComponent = component; + }); } if (!this.singleSectionComponent) { - this.singleSectionComponent = this.cfDelegate.getSingleSectionComponent(this.course); + this.cfDelegate.getSingleSectionComponent(this.injector, this.course).then((component) => { + this.singleSectionComponent = component; + }); } if (!this.allSectionsComponent) { - this.allSectionsComponent = this.cfDelegate.getAllSectionsComponent(this.course); + this.cfDelegate.getAllSectionsComponent(this.injector, this.course).then((component) => { + this.allSectionsComponent = component; + }); } } } diff --git a/src/core/course/components/module-completion/module-completion.ts b/src/core/course/components/module-completion/module-completion.ts index 6290ae798..1389ff1b7 100644 --- a/src/core/course/components/module-completion/module-completion.ts +++ b/src/core/course/components/module-completion/module-completion.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreUserProvider } from '../../../user/providers/user'; +import { CoreUserProvider } from '@core/user/providers/user'; /** * Component to handle activity completion. It shows a checkbox with the current status, and allows manually changing diff --git a/src/core/course/components/module/module.ts b/src/core/course/components/module/module.ts index 00d0bbf1f..fef73a640 100644 --- a/src/core/course/components/module/module.ts +++ b/src/core/course/components/module/module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; +import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; @@ -67,7 +67,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { protected prefetchHandler: CoreCourseModulePrefetchHandler; protected statusObserver; - constructor(protected navCtrl: NavController, protected prefetchDelegate: CoreCourseModulePrefetchDelegate, + constructor(@Optional() protected navCtrl: NavController, protected prefetchDelegate: CoreCourseModulePrefetchDelegate, protected domUtils: CoreDomUtilsProvider, protected courseHelper: CoreCourseHelperProvider, protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider) { this.completionChanged = new EventEmitter(); diff --git a/src/core/course/course.module.ts b/src/core/course/course.module.ts index 9154daaab..d26fbd730 100644 --- a/src/core/course/course.module.ts +++ b/src/core/course/course.module.ts @@ -25,6 +25,16 @@ import { CoreCourseFormatSocialModule } from './formats/social/social.module'; import { CoreCourseFormatTopicsModule } from './formats/topics/topics.module'; import { CoreCourseFormatWeeksModule } from './formats/weeks/weeks.module'; +// List of providers (without handlers). +export const CORE_COURSE_PROVIDERS: any[] = [ + CoreCourseProvider, + CoreCourseHelperProvider, + CoreCourseFormatDelegate, + CoreCourseModuleDelegate, + CoreCourseModulePrefetchDelegate, + CoreCourseOptionsDelegate +]; + @NgModule({ declarations: [], imports: [ @@ -33,15 +43,9 @@ import { CoreCourseFormatWeeksModule } from './formats/weeks/weeks.module'; CoreCourseFormatWeeksModule, CoreCourseFormatSocialModule ], - providers: [ - CoreCourseProvider, - CoreCourseHelperProvider, - CoreCourseFormatDelegate, - CoreCourseModuleDelegate, - CoreCourseModulePrefetchDelegate, - CoreCourseOptionsDelegate, + providers: CORE_COURSE_PROVIDERS.concat([ CoreCourseFormatDefaultHandler - ], + ]), exports: [] }) export class CoreCourseModule {} diff --git a/src/core/course/directives/directives.module.ts b/src/core/course/directives/directives.module.ts new file mode 100644 index 000000000..9b43d41cc --- /dev/null +++ b/src/core/course/directives/directives.module.ts @@ -0,0 +1,27 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreCourseDownloadModuleMainFileDirective } from './download-module-main-file'; + +@NgModule({ + declarations: [ + CoreCourseDownloadModuleMainFileDirective + ], + imports: [], + exports: [ + CoreCourseDownloadModuleMainFileDirective + ] +}) +export class CoreCourseDirectivesModule {} diff --git a/src/core/course/directives/download-module-main-file.ts b/src/core/course/directives/download-module-main-file.ts new file mode 100644 index 000000000..80fe1fb99 --- /dev/null +++ b/src/core/course/directives/download-module-main-file.ts @@ -0,0 +1,81 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Directive, Input, OnInit, ElementRef } from '@angular/core'; +import { CoreCourseProvider } from '../providers/course'; +import { CoreCourseHelperProvider } from '../providers/helper'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; + +/** + * Directive to allow downloading and open the main file of a module. + * When the item with this directive is clicked, the module will be downloaded (if needed) and opened. + * This is meant for modules like mod_resource. + * + * This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents. + */ +@Directive({ + selector: '[core-course-download-module-main-file]' +}) +export class CoreCourseDownloadModuleMainFileDirective implements OnInit { + @Input() module: any; // The module. + @Input() moduleId: string | number; // The module ID. Required if module is not supplied. + @Input() courseId: string | number; // The course ID. + @Input() component?: string; // Component to link the file to. + @Input() componentId?: string | number; // Component ID to use in conjunction with the component. If not defined, use moduleId. + @Input() files?: any[]; // List of files of the module. If not provided, use module.contents. + + protected element: HTMLElement; + + constructor(element: ElementRef, protected domUtils: CoreDomUtilsProvider, protected courseHelper: CoreCourseHelperProvider, + protected courseProvider: CoreCourseProvider) { + this.element = element.nativeElement || element; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.element.addEventListener('click', (ev: Event): void => { + if (!this.module && !this.moduleId) { + return; + } + + ev.preventDefault(); + ev.stopPropagation(); + + const modal = this.domUtils.showModalLoading(), + courseId = typeof this.courseId == 'string' ? parseInt(this.courseId, 10) : this.courseId; + let promise; + + if (this.module) { + // We already have the module. + promise = Promise.resolve(module); + } else { + // Try to get the module from cache. + this.moduleId = typeof this.moduleId == 'string' ? parseInt(this.moduleId, 10) : this.moduleId; + promise = this.courseProvider.getModule(this.moduleId, courseId); + } + + promise.then((module) => { + const componentId = this.componentId || module.id; + + return this.courseHelper.downloadModuleAndOpenFile(module, courseId, this.component, componentId, this.files); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); + }).finally(() => { + modal.dismiss(); + }); + }); + } +} diff --git a/src/core/course/formats/singleactivity/components/singleactivity.ts b/src/core/course/formats/singleactivity/components/singleactivity.ts index a94f8730f..a68a60fda 100644 --- a/src/core/course/formats/singleactivity/components/singleactivity.ts +++ b/src/core/course/formats/singleactivity/components/singleactivity.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnChanges, SimpleChange, ViewChild } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChange, ViewChild, Injector } from '@angular/core'; import { CoreCourseModuleDelegate } from '../../../providers/module-delegate'; import { CoreCourseUnsupportedModuleComponent } from '../../../components/unsupported-module/unsupported-module'; import { CoreDynamicComponent } from '../../../../../components/dynamic-component/dynamic-component'; @@ -36,7 +36,7 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges { componentClass: any; // The class of the component to render. data: any = {}; // Data to pass to the component. - constructor(private moduleDelegate: CoreCourseModuleDelegate) { } + constructor(private moduleDelegate: CoreCourseModuleDelegate, private injector: Injector) { } /** * Detect changes on input properties. @@ -47,8 +47,9 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges { const module = this.sections[0] && this.sections[0].modules && this.sections[0].modules[0]; if (module && !this.componentClass) { // We haven't obtained the class yet. Get it now. - this.componentClass = this.moduleDelegate.getMainComponent(this.course, module) || - CoreCourseUnsupportedModuleComponent; + this.moduleDelegate.getMainComponent(this.injector, this.course, module).then((component) => { + this.componentClass = component || CoreCourseUnsupportedModuleComponent; + }); } this.data.courseId = this.course.id; diff --git a/src/core/course/formats/singleactivity/providers/handler.ts b/src/core/course/formats/singleactivity/providers/handler.ts index ed7d44eef..9dd528021 100644 --- a/src/core/course/formats/singleactivity/providers/handler.ts +++ b/src/core/course/formats/singleactivity/providers/handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreCourseFormatHandler } from '../../../providers/format-delegate'; import { CoreCourseFormatSingleActivityComponent } from '../components/singleactivity'; @@ -86,11 +86,13 @@ export class CoreCourseFormatSingleActivityHandler implements CoreCourseFormatHa * Return the Component to use to display the course format instead of using the default one. * Use it if you want to display a format completely different from the default one. * If you want to customize the default format there are several methods to customize parts of it. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getCourseFormatComponent(course: any): any { + getCourseFormatComponent(injector: Injector, course: any): any | Promise { return CoreCourseFormatSingleActivityComponent; } } diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index 83bf61d17..56cd025b4 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -30,7 +30,7 @@ - + diff --git a/src/core/course/pages/section/section.module.ts b/src/core/course/pages/section/section.module.ts index bfa4d3eb3..db558c4c3 100644 --- a/src/core/course/pages/section/section.module.ts +++ b/src/core/course/pages/section/section.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreCourseSectionPage } from './section'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreCourseComponentsModule } from '../../components/components.module'; @NgModule({ diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 0e0ced8b7..b5d5806ca 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild, OnDestroy } from '@angular/core'; +import { Component, ViewChild, OnDestroy, Injector } from '@angular/core'; import { IonicPage, NavParams, Content, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; @@ -25,7 +25,7 @@ import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; import { CoreCourseModulePrefetchDelegate } from '../../providers/module-prefetch-delegate'; import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay } from '../../providers/options-delegate'; import { CoreCourseFormatComponent } from '../../components/format/format'; -import { CoreCoursesProvider } from '../../../courses/providers/courses'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; /** * Page that displays the list of courses the user is enrolled in. @@ -45,7 +45,6 @@ export class CoreCourseSectionPage implements OnDestroy { sectionId: number; sectionNumber: number; courseHandlers: CoreCourseOptionsHandlerToDisplay[]; - handlerData: any = {}; // Data to send to the handlers components. dataLoaded: boolean; downloadEnabled: boolean; downloadEnabledIcon = 'square-outline'; // Disabled by default. @@ -64,13 +63,12 @@ export class CoreCourseSectionPage implements OnDestroy { private courseFormatDelegate: CoreCourseFormatDelegate, private courseOptionsDelegate: CoreCourseOptionsDelegate, private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider, - sitesProvider: CoreSitesProvider, private navCtrl: NavController, + sitesProvider: CoreSitesProvider, private navCtrl: NavController, private injector: Injector, private prefetchDelegate: CoreCourseModulePrefetchDelegate) { this.course = navParams.get('course'); this.sectionId = navParams.get('sectionId'); this.sectionNumber = navParams.get('sectionNumber'); this.module = navParams.get('module'); - this.handlerData.courseId = this.course.id; // Get the title to display. We dont't have sections yet. this.title = courseFormatDelegate.getCourseTitle(this.course); @@ -193,7 +191,14 @@ export class CoreCourseSectionPage implements OnDestroy { })); // Load the course handlers. - promises.push(this.courseOptionsDelegate.getHandlersToDisplay(this.course, refresh, false).then((handlers) => { + promises.push(this.courseOptionsDelegate.getHandlersToDisplay(this.injector, this.course, refresh, false) + .then((handlers) => { + // Add the courseId to the handler component data. + handlers.forEach((handler) => { + handler.data.componentData = handler.data.componentData || {}; + handler.data.componentData.courseId = this.course.id; + }); + this.courseHandlers = handlers; })); diff --git a/src/core/course/pages/unsupported-module/unsupported-module.module.ts b/src/core/course/pages/unsupported-module/unsupported-module.module.ts index 5d2ca24a9..d3772a0da 100644 --- a/src/core/course/pages/unsupported-module/unsupported-module.module.ts +++ b/src/core/course/pages/unsupported-module/unsupported-module.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreCourseUnsupportedModulePage } from './unsupported-module'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreCourseComponentsModule } from '../../components/components.module'; @NgModule({ diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index 9239b8921..083ed039f 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -201,7 +201,7 @@ export class CoreCourseProvider { * @return {Promise} Promise resolved with the module. */ getModule(moduleId: number, courseId?: number, sectionId?: number, preferCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); let promise; diff --git a/src/core/course/providers/default-format.ts b/src/core/course/providers/default-format.ts index 2bf1991d1..2e9a58a17 100644 --- a/src/core/course/providers/default-format.ts +++ b/src/core/course/providers/default-format.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { NavController } from 'ionic-angular'; -import { CoreCoursesProvider } from '../../courses/providers/courses'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCourseFormatHandler } from './format-delegate'; import { CoreCourseProvider } from './course'; diff --git a/src/core/course/providers/format-delegate.ts b/src/core/course/providers/format-delegate.ts index 2398f4192..55f064f2a 100644 --- a/src/core/course/providers/format-delegate.ts +++ b/src/core/course/providers/format-delegate.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; @@ -85,44 +85,54 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { * Return the Component to use to display the course format instead of using the default one. * Use it if you want to display a format completely different from the default one. * If you want to customize the default format there are several methods to customize parts of it. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getCourseFormatComponent?(course: any): any; + getCourseFormatComponent?(injector: Injector, course: any): any | Promise; /** * Return the Component to use to display the course summary inside the default course format. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getCourseSummaryComponent?(course: any): any; + getCourseSummaryComponent?(injector: Injector, course: any): any | Promise; /** * Return the Component to use to display the section selector inside the default course format. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getSectionSelectorComponent?(course: any): any; + getSectionSelectorComponent?(injector: Injector, course: any): any | Promise; /** * Return the Component to use to display a single section. This component will only be used if the user is viewing a * single section. If all the sections are displayed at once then it won't be used. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getSingleSectionComponent?(course: any): any; + getSingleSectionComponent?(injector: Injector, course: any): any | Promise; /** * Return the Component to use to display all sections in a course. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getAllSectionsComponent?(course: any): any; + getAllSectionsComponent?(injector: Injector, course: any): any | Promise; /** * Invalidate the data required to load the course format. @@ -199,31 +209,40 @@ export class CoreCourseFormatDelegate extends CoreDelegate { /** * Get the component to use to display all sections in a course. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getAllSectionsComponent(course: any): any { - return this.executeFunction(course.format, 'getAllSectionsComponent', [course]); + getAllSectionsComponent(injector: Injector, course: any): Promise { + return Promise.resolve(this.executeFunction(course.format, 'getAllSectionsComponent', [injector, course])).catch((e) => { + this.logger.error('Error getting all sections component', e); + }); } /** * Get the component to use to display a course format. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getCourseFormatComponent(course: any): any { - return this.executeFunction(course.format, 'getCourseFormatComponent', [course]); + getCourseFormatComponent(injector: Injector, course: any): Promise { + return Promise.resolve(this.executeFunction(course.format, 'getCourseFormatComponent', [injector, course])).catch((e) => { + this.logger.error('Error getting course format component', e); + }); } /** * Get the component to use to display the course summary in the default course format. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getCourseSummaryComponent(course: any): any { - return this.executeFunction(course.format, 'getCourseSummaryComponent', [course]); + getCourseSummaryComponent(injector: Injector, course: any): Promise { + return Promise.resolve(this.executeFunction(course.format, 'getCourseSummaryComponent', [injector, course])).catch((e) => { + this.logger.error('Error getting course summary component', e); + }); } /** @@ -259,22 +278,29 @@ export class CoreCourseFormatDelegate extends CoreDelegate { /** * Get the component to use to display the section selector inside the default course format. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getSectionSelectorComponent(course: any): any { - return this.executeFunction(course.format, 'getSectionSelectorComponent', [course]); + getSectionSelectorComponent(injector: Injector, course: any): Promise { + return Promise.resolve(this.executeFunction(course.format, 'getSectionSelectorComponent', [injector, course])) + .catch((e) => { + this.logger.error('Error getting section selector component', e); + }); } /** * Get the component to use to display a single section. This component will only be used if the user is viewing * a single section. If all the sections are displayed at once then it won't be used. * + * @param {Injector} injector Injector. * @param {any} course The course to render. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getSingleSectionComponent(course: any): any { - return this.executeFunction(course.format, 'getSingleSectionComponent', [course]); + getSingleSectionComponent(injector: Injector, course: any): Promise { + return Promise.resolve(this.executeFunction(course.format, 'getSingleSectionComponent', [injector, course])).catch((e) => { + this.logger.error('Error getting single section component', e); + }); } /** diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index fedffa140..236b227dd 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -12,23 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; +import { CoreFileProvider } from '@providers/file'; import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreFileHelperProvider } from '@providers/file-helper'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay } from './options-delegate'; -import { CoreSiteHomeProvider } from '../../sitehome/providers/sitehome'; +import { CoreSiteHomeProvider } from '@core/sitehome/providers/sitehome'; import { CoreCourseProvider } from './course'; import { CoreCourseModuleDelegate } from './module-delegate'; import { CoreCourseModulePrefetchDelegate } from './module-prefetch-delegate'; -import { CoreLoginHelperProvider } from '../../login/providers/helper'; -import { CoreConstants } from '../../constants'; +import { CoreLoginHelperProvider } from '@core/login/providers/helper'; +import { CoreConstants } from '@core/constants'; import { CoreSite } from '@classes/site'; import * as moment from 'moment'; @@ -116,7 +119,8 @@ export class CoreCourseHelperProvider { private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private utils: CoreUtilsProvider, private translate: TranslateService, private loginHelper: CoreLoginHelperProvider, private courseOptionsDelegate: CoreCourseOptionsDelegate, private siteHomeProvider: CoreSiteHomeProvider, - private eventsProvider: CoreEventsProvider) { } + private eventsProvider: CoreEventsProvider, private fileHelper: CoreFileHelperProvider, + private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, private injector: Injector) { } /** * This function treats every module on the sections provided to load the handler data, treat completion @@ -271,7 +275,7 @@ export class CoreCourseHelperProvider { if (courseHandlers) { promise = Promise.resolve(courseHandlers); } else { - promise = this.courseOptionsDelegate.getHandlersToDisplay(course); + promise = this.courseOptionsDelegate.getHandlersToDisplay(this.injector, course); } return promise.then((handlers: CoreCourseOptionsHandlerToDisplay[]) => { @@ -319,7 +323,7 @@ export class CoreCourseHelperProvider { subPromises.push(this.courseProvider.getSections(course.id, false, true).then((courseSections) => { sections = courseSections; })); - subPromises.push(this.courseOptionsDelegate.getHandlersToDisplay(course).then((cHandlers) => { + subPromises.push(this.courseOptionsDelegate.getHandlersToDisplay(this.injector, course).then((cHandlers) => { handlers = cHandlers; })); @@ -470,6 +474,227 @@ export class CoreCourseHelperProvider { }); } + /** + * Convenience function to open a module main file, downloading the package if needed. + * This is meant for modules like mod_resource. + * + * @param {any} module The module to download. + * @param {number} courseId The course ID of the module. + * @param {string} [component] The component to link the files to. + * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {any[]} [files] List of files of the module. If not provided, use module.contents. + * @param {string} [siteId] The site ID. If not defined, current site. + * @return {Promise} Resolved on success. + */ + downloadModuleAndOpenFile(module: any, courseId: number, component?: string, componentId?: string | number, files?: any[], + siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + let promise; + if (files) { + promise = Promise.resolve(files); + } else { + promise = this.courseProvider.loadModuleContents(module, courseId).then(() => { + files = module.contents; + }); + } + + // Make sure that module contents are loaded. + return promise.then(() => { + if (!files || !files.length) { + return Promise.reject(null); + } + + return this.sitesProvider.getSite(siteId); + }).then((site) => { + const mainFile = files[0], + fileUrl = this.fileHelper.getFileUrl(mainFile); + + // Check if the file should be opened in browser. + if (this.fileHelper.shouldOpenInBrowser(mainFile)) { + if (this.appProvider.isOnline()) { + // Open in browser. + let fixedUrl = site.fixPluginfileURL(fileUrl).replace('&offline=1', ''); + // Remove forcedownload when followed by another param. + fixedUrl = fixedUrl.replace(/forcedownload=\d+&/, ''); + // Remove forcedownload when not followed by any param. + fixedUrl = fixedUrl.replace(/[\?|\&]forcedownload=\d+/, ''); + + this.utils.openInBrowser(fixedUrl); + + if (this.fileProvider.isAvailable()) { + // Download the file if needed (file outdated or not downloaded). + // Download will be in background, don't return the promise. + this.downloadModule(module, courseId, component, componentId, files, siteId); + } + + return; + } else { + // Not online, get the offline file. It will fail if not found. + return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl).then((path) => { + return this.utils.openFile(path); + }).catch((error) => { + return Promise.reject(this.translate.instant('core.networkerrormsg')); + }); + } + } + + // File shouldn't be opened in browser. Download the module if it needs to be downloaded. + return this.downloadModuleWithMainFileIfNeeded(module, courseId, component, componentId, files, siteId) + .then((result) => { + if (result.path.indexOf('http') === 0) { + return this.utils.openOnlineFile(result.path).catch((error) => { + // Error opening the file, some apps don't allow opening online files. + if (!this.fileProvider.isAvailable()) { + return Promise.reject(error); + } else if (result.status === CoreConstants.DOWNLOADING) { + return Promise.reject(this.translate.instant('core.erroropenfiledownloading')); + } + + let promise; + if (result.status === CoreConstants.NOT_DOWNLOADED) { + // Not downloaded, download it now and return the local file. + promise = this.downloadModule(module, courseId, component, componentId, files, siteId).then(() => { + return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl); + }); + } else { + // File is outdated or stale and can't be opened in online, return the local URL. + promise = this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl); + } + + return promise.then((path) => { + return this.utils.openFile(path); + }); + }); + } else { + return this.utils.openFile(result.path); + } + }); + }); + } + + /** + * Convenience function to download a module that has a main file and return the local file's path and other info. + * This is meant for modules like mod_resource. + * + * @param {any} module The module to download. + * @param {number} courseId The course ID of the module. + * @param {string} [component] The component to link the files to. + * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {any[]} [files] List of files of the module. If not provided, use module.contents. + * @param {string} [siteId] The site ID. If not defined, current site. + * @return {Promise<{fixedUrl: string, path: string, status: string}>} Promise resolved when done. + */ + protected downloadModuleWithMainFileIfNeeded(module: any, courseId: number, component?: string, componentId?: string | number, + files?: any[], siteId?: string): Promise<{fixedUrl: string, path: string, status: string}> { + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (!files || !files.length) { + // Module not valid, stop. + return Promise.reject(null); + } + + const mainFile = files[0], + fileUrl = this.fileHelper.getFileUrl(mainFile), + timemodified = this.fileHelper.getFileTimemodified(mainFile), + prefetchHandler = this.prefetchDelegate.getPrefetchHandlerFor(module), + result = { + fixedUrl: undefined, + path: undefined, + status: undefined + }; + + return this.sitesProvider.getSite(siteId).then((site) => { + const fixedUrl = site.fixPluginfileURL(fileUrl); + result.fixedUrl = fixedUrl; + + if (this.fileProvider.isAvailable()) { + // The file system is available. + return this.filepoolProvider.getPackageStatus(siteId, component, componentId).then((status) => { + result.status = status; + + const isWifi = !this.appProvider.isNetworkAccessLimited(), + isOnline = this.appProvider.isOnline(); + + if (status === CoreConstants.DOWNLOADED) { + // Get the local file URL. + return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl); + } else if (status === CoreConstants.DOWNLOADING && !this.appProvider.isDesktop()) { + // Return the online URL. + return fixedUrl; + } else { + if (!isOnline && status === CoreConstants.NOT_DOWNLOADED) { + // Not downloaded and we're offline, reject. + return Promise.reject(this.translate.instant('core.networkerrormsg')); + } + + return this.filepoolProvider.shouldDownloadBeforeOpen(fixedUrl, mainFile.filesize).then(() => { + // Download and then return the local URL. + return this.downloadModule(module, courseId, component, componentId, files, siteId).then(() => { + return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl); + }); + }, () => { + // Start the download if in wifi, but return the URL right away so the file is opened. + if (isWifi && isOnline) { + this.downloadModule(module, courseId, component, componentId, files, siteId); + } + + if (!this.fileHelper.isStateDownloaded(status) || isOnline) { + // Not downloaded or online, return the online URL. + return fixedUrl; + } else { + // Outdated but offline, so we return the local URL. Use getUrlByUrl so it's added to the queue. + return this.filepoolProvider.getUrlByUrl(siteId, fileUrl, component, componentId, timemodified, + false, false, mainFile); + } + }); + } + }).then((path) => { + result.path = path; + + return result; + }); + } else { + // We use the live URL. + result.path = fixedUrl; + + return result; + } + }); + } + + /** + * Convenience function to download a module. + * + * @param {any} module The module to download. + * @param {number} courseId The course ID of the module. + * @param {string} [component] The component to link the files to. + * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {any[]} [files] List of files of the module. If not provided, use module.contents. + * @param {string} [siteId] The site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + downloadModule(module: any, courseId: number, component?: string, componentId?: string | number, files?: any[], siteId?: string) + : Promise { + + const prefetchHandler = this.prefetchDelegate.getPrefetchHandlerFor(module); + + if (prefetchHandler) { + // Use the prefetch handler to download the module. + if (prefetchHandler.download) { + return prefetchHandler.download(module, courseId); + } else { + return prefetchHandler.prefetch(module, courseId, true); + } + } + + // There's no prefetch handler for the module, just download the files. + files = files || module.contents; + + return this.filepoolProvider.downloadOrPrefetchFiles(siteId, files, false, false, component, componentId); + } + /** * Fill the Context Menu for a certain module. * @@ -605,8 +830,13 @@ export class CoreCourseHelperProvider { moduleInfo.statusIcon = 'spinner'; break; case CoreConstants.OUTDATED: - moduleInfo.statusIcon = 'ion-android-refresh'; + moduleInfo.statusIcon = 'refresh'; break; + case CoreConstants.DOWNLOADED: + if (!this.prefetchDelegate.canCheckUpdates()) { + moduleInfo.statusIcon = 'refresh'; + break; + } default: moduleInfo.statusIcon = ''; break; diff --git a/src/core/course/providers/module-delegate.ts b/src/core/course/providers/module-delegate.ts index 53f910b08..27cc1eab8 100644 --- a/src/core/course/providers/module-delegate.ts +++ b/src/core/course/providers/module-delegate.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController, NavOptions } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; @@ -38,12 +38,14 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler { /** * Get the component to render the module. This is needed to support singleactivity course format. * The component returned must implement CoreCourseModuleMainComponent. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @param {any} course The course object. * @param {any} module The module object. - * @return {any} The component to use, undefined if not found. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getMainComponent(course: any, module: any): any; + getMainComponent(injector: Injector, course: any, module: any): any | Promise; } /** @@ -176,17 +178,17 @@ export class CoreCourseModuleDelegate extends CoreDelegate { /** * Get the component to render the module. * + * @param {Injector} injector Injector. * @param {any} course The course object. * @param {any} module The module object. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getMainComponent?(course: any, module: any): any { + getMainComponent(injector: Injector, course: any, module: any): Promise { const handler = this.enabledHandlers[module.modname]; if (handler && handler.getMainComponent) { - const component = handler.getMainComponent(course, module); - if (component) { - return component; - } + return Promise.resolve(handler.getMainComponent(injector, course, module)).catch((err) => { + this.logger.error('Error getting main component', err); + }); } } diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index 76589a93e..84f4b0b15 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -86,9 +86,20 @@ export interface CoreCourseModulePrefetchHandler extends CoreDelegateHandler { * @param {any} module Module. * @param {number} courseId Course ID the module belongs to. * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} [dirPath] Path of the directory where to store all the content files. * @return {Promise} Promise resolved when done. */ - prefetch(module: any, courseId?: number, single?: boolean): Promise; + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise; + + /** + * Download the module. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {string} [dirPath] Path of the directory where to store all the content files. + * @return {Promise} Promise resolved when all content is downloaded. + */ + download?(module: any, courseId: number, dirPath?: string): Promise; /** * Check if a certain module can use core_course_check_updates to check if it has updates. @@ -141,8 +152,8 @@ export interface CoreCourseModulePrefetchHandler extends CoreDelegateHandler { hasUpdates?(module: any, courseId: number, moduleUpdates: any[]): boolean | Promise; /** - * Invalidate WS calls needed to determine module status. It doesn't need to invalidate check updates. - * It should NOT invalidate files nor all the prefetched data. + * Invalidate WS calls needed to determine module status (usually, to check if module is downloadable). + * It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data. * * @param {any} module Module. * @param {number} courseId Course ID the module belongs to. diff --git a/src/core/course/providers/options-delegate.ts b/src/core/course/providers/options-delegate.ts index 35182a369..569476d70 100644 --- a/src/core/course/providers/options-delegate.ts +++ b/src/core/course/providers/options-delegate.ts @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider, PromiseDefer } from '@providers/utils/utils'; -import { CoreCoursesProvider } from '../../courses/providers/courses'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCourseProvider } from './course'; /** @@ -45,10 +45,11 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler { /** * Returns the data needed to render the handler. * + * @param {Injector} injector Injector. * @param {number} courseId The course ID. - * @return {CoreCourseOptionsHandlerData} Data. + * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(courseId: number): CoreCourseOptionsHandlerData; + getDisplayData?(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise; /** * Should invalidate the data to determine if the handler is enabled for a certain course. @@ -90,6 +91,12 @@ export interface CoreCourseOptionsHandlerData { * When the component is created, it will receive the courseId as input. */ component: any; + + /** + * Data to pass to the component. All the properties in this object will be passed to the component as inputs. + * @type {any} + */ + componentData?: any; } /** @@ -237,6 +244,7 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { * Get the list of handlers that should be displayed for a course. * This function should be called only when the handlers need to be displayed, since it can call several WebServices. * + * @param {Injector} injector Injector. * @param {any} course The course object. * @param {boolean} [refresh] True if it should refresh the list. * @param {boolean} [isGuest] Whether it's guest. @@ -244,7 +252,7 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. * @return {Promise} Promise resolved with array of handlers. */ - getHandlersToDisplay(course: any, refresh?: boolean, isGuest?: boolean, navOptions?: any, admOptions?: any): + getHandlersToDisplay(injector: Injector, course: any, refresh?: boolean, isGuest?: boolean, navOptions?: any, admOptions?: any): Promise { course.id = parseInt(course.id, 10); @@ -263,14 +271,19 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { // Call getHandlersForAccess to make sure the handlers have been loaded. return this.getHandlersForAccess(course.id, refresh, accessData, course.navOptions, course.admOptions); }).then(() => { - const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] = []; + const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] = [], + promises = []; this.coursesHandlers[course.id].enabledHandlers.forEach((handler) => { - handlersToDisplay.push({ - data: handler.getDisplayData(course), - priority: handler.priority, - prefetch: handler.prefetch - }); + promises.push(Promise.resolve(handler.getDisplayData(injector, course)).then((data) => { + handlersToDisplay.push({ + data: data, + priority: handler.priority, + prefetch: handler.prefetch + }); + }).catch((err) => { + this.logger.error('Error getting data for handler', handler.name, err); + })); }); // Sort them by priority. diff --git a/src/core/courses/components/components.module.ts b/src/core/courses/components/components.module.ts index d8dbe5a35..d873b3d82 100644 --- a/src/core/courses/components/components.module.ts +++ b/src/core/courses/components/components.module.ts @@ -17,8 +17,8 @@ import { CommonModule } from '@angular/common'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; -import { CorePipesModule } from '@pipes'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; import { CoreCoursesCourseProgressComponent } from '../components/course-progress/course-progress'; import { CoreCoursesCourseListItemComponent } from '../components/course-list-item/course-list-item'; import { CoreCoursesOverviewEventsComponent } from '../components/overview-events/overview-events'; diff --git a/src/core/courses/components/course-list-item/course-list-item.ts b/src/core/courses/components/course-list-item/course-list-item.ts index d3805a1f0..3a6e47343 100644 --- a/src/core/courses/components/course-list-item/course-list-item.ts +++ b/src/core/courses/components/course-list-item/course-list-item.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreCoursesProvider } from '../../providers/courses'; @@ -31,7 +31,8 @@ import { CoreCoursesProvider } from '../../providers/courses'; export class CoreCoursesCourseListItemComponent implements OnInit { @Input() course: any; // The course to render. - constructor(private navCtrl: NavController, private translate: TranslateService, private coursesProvider: CoreCoursesProvider) { + constructor(@Optional() private navCtrl: NavController, private translate: TranslateService, + private coursesProvider: CoreCoursesProvider) { } /** diff --git a/src/core/courses/components/course-progress/course-progress.ts b/src/core/courses/components/course-progress/course-progress.ts index f5152e3ff..c0690dba1 100644 --- a/src/core/courses/components/course-progress/course-progress.ts +++ b/src/core/courses/components/course-progress/course-progress.ts @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit, OnDestroy } from '@angular/core'; +import { Component, Input, OnInit, OnDestroy, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate'; -import { CoreCourseProvider } from '../../../course/providers/course'; -import { CoreCourseHelperProvider } from '../../../course/providers/helper'; +import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; /** * This component is meant to display a course for a list of courses with progress. @@ -44,7 +44,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { protected isDestroyed = false; protected courseStatusObserver; - constructor(private navCtrl: NavController, private courseHelper: CoreCourseHelperProvider, + constructor(@Optional() private navCtrl: NavController, private courseHelper: CoreCourseHelperProvider, private courseFormatDelegate: CoreCourseFormatDelegate, private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider) { // Listen for status change in course. diff --git a/src/core/courses/components/overview-events/overview-events.ts b/src/core/courses/components/overview-events/overview-events.ts index 4ffc4e3fb..cf59b3f49 100644 --- a/src/core/courses/components/overview-events/overview-events.ts +++ b/src/core/courses/components/overview-events/overview-events.ts @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, OnChanges, EventEmitter, SimpleChange } from '@angular/core'; +import { Component, Input, Output, OnChanges, EventEmitter, SimpleChange, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreCourseProvider } from '../../../course/providers/course'; -import { CoreContentLinksHelperProvider } from '../../../contentlinks/providers/helper'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import * as moment from 'moment'; /** @@ -43,9 +43,10 @@ export class CoreCoursesOverviewEventsComponent implements OnChanges { next30Days: any[] = []; future: any[] = []; - constructor(private navCtrl: NavController, private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, - private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider, - private courseProvider: CoreCourseProvider, private contentLinksHelper: CoreContentLinksHelperProvider) { + constructor(@Optional() private navCtrl: NavController, private utils: CoreUtilsProvider, + private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider, + private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider, + private contentLinksHelper: CoreContentLinksHelperProvider) { this.loadMore = new EventEmitter(); } diff --git a/src/core/courses/courses.module.ts b/src/core/courses/courses.module.ts index 2fde4b912..0156449fb 100644 --- a/src/core/courses/courses.module.ts +++ b/src/core/courses/courses.module.ts @@ -19,21 +19,25 @@ import { CoreCoursesMyOverviewProvider } from './providers/my-overview'; import { CoreCoursesCourseLinkHandler } from './providers/course-link-handler'; import { CoreCoursesIndexLinkHandler } from './providers/courses-index-link-handler'; import { CoreCoursesMyOverviewLinkHandler } from './providers/my-overview-link-handler'; -import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; -import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; +import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; + +// List of providers (without handlers). +export const CORE_COURSES_PROVIDERS: any[] = [ + CoreCoursesProvider, + CoreCoursesMyOverviewProvider +]; @NgModule({ declarations: [], imports: [ ], - providers: [ - CoreCoursesProvider, + providers: CORE_COURSES_PROVIDERS.concat([ CoreCoursesMainMenuHandler, - CoreCoursesMyOverviewProvider, CoreCoursesCourseLinkHandler, CoreCoursesIndexLinkHandler, CoreCoursesMyOverviewLinkHandler - ], + ]), exports: [] }) export class CoreCoursesModule { diff --git a/src/core/courses/pages/categories/categories.module.ts b/src/core/courses/pages/categories/categories.module.ts index 2e27333b6..c60b37cc3 100644 --- a/src/core/courses/pages/categories/categories.module.ts +++ b/src/core/courses/pages/categories/categories.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreCoursesCategoriesPage } from './categories'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreCoursesComponentsModule } from '../../components/components.module'; @NgModule({ diff --git a/src/core/courses/pages/course-preview/course-preview.module.ts b/src/core/courses/pages/course-preview/course-preview.module.ts index 9b65092c2..2a34ddf92 100644 --- a/src/core/courses/pages/course-preview/course-preview.module.ts +++ b/src/core/courses/pages/course-preview/course-preview.module.ts @@ -17,8 +17,8 @@ import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreCoursesCoursePreviewPage } from './course-preview'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; -import { CorePipesModule } from '@pipes'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; @NgModule({ declarations: [ diff --git a/src/core/courses/pages/course-preview/course-preview.ts b/src/core/courses/pages/course-preview/course-preview.ts index a7877ece2..288baabcf 100644 --- a/src/core/courses/pages/course-preview/course-preview.ts +++ b/src/core/courses/pages/course-preview/course-preview.ts @@ -21,9 +21,9 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreCoursesProvider } from '../../providers/courses'; -import { CoreCourseOptionsDelegate } from '../../../course/providers/options-delegate'; -import { CoreCourseProvider } from '../../../course/providers/course'; -import { CoreCourseHelperProvider } from '../../../course/providers/helper'; +import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; /** * Page that allows "previewing" a course and enrolling in it if enabled and not enrolled. diff --git a/src/core/courses/pages/my-courses/my-courses.ts b/src/core/courses/pages/my-courses/my-courses.ts index 9ad191833..1508052d4 100644 --- a/src/core/courses/pages/my-courses/my-courses.ts +++ b/src/core/courses/pages/my-courses/my-courses.ts @@ -18,8 +18,8 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCoursesProvider } from '../../providers/courses'; -import { CoreCourseHelperProvider } from '../../../course/providers/helper'; -import { CoreCourseOptionsDelegate } from '../../../course/providers/options-delegate'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; /** * Page that displays the list of courses the user is enrolled in. diff --git a/src/core/courses/pages/my-overview/my-overview.module.ts b/src/core/courses/pages/my-overview/my-overview.module.ts index 8825f5ab0..9c1611b8e 100644 --- a/src/core/courses/pages/my-overview/my-overview.module.ts +++ b/src/core/courses/pages/my-overview/my-overview.module.ts @@ -18,7 +18,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreCoursesMyOverviewPage } from './my-overview'; import { CoreComponentsModule } from '@components/components.module'; import { CoreCoursesComponentsModule } from '../../components/components.module'; -import { CoreSiteHomeComponentsModule } from '../../../sitehome/components/components.module'; +import { CoreSiteHomeComponentsModule } from '@core/sitehome/components/components.module'; @NgModule({ declarations: [ diff --git a/src/core/courses/pages/my-overview/my-overview.ts b/src/core/courses/pages/my-overview/my-overview.ts index a90972fb1..552208dfa 100644 --- a/src/core/courses/pages/my-overview/my-overview.ts +++ b/src/core/courses/pages/my-overview/my-overview.ts @@ -18,9 +18,9 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCoursesProvider } from '../../providers/courses'; import { CoreCoursesMyOverviewProvider } from '../../providers/my-overview'; -import { CoreCourseHelperProvider } from '../../../course/providers/helper'; -import { CoreCourseOptionsDelegate } from '../../../course/providers/options-delegate'; -import { CoreSiteHomeProvider } from '../../../sitehome/providers/sitehome'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; +import { CoreSiteHomeProvider } from '@core/sitehome/providers/sitehome'; import * as moment from 'moment'; /** diff --git a/src/core/courses/pages/self-enrol-password/self-enrol-password.module.ts b/src/core/courses/pages/self-enrol-password/self-enrol-password.module.ts index d1aaa6da6..3c24736b0 100644 --- a/src/core/courses/pages/self-enrol-password/self-enrol-password.module.ts +++ b/src/core/courses/pages/self-enrol-password/self-enrol-password.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { CoreCoursesSelfEnrolPasswordPage } from './self-enrol-password'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/courses/providers/course-link-handler.ts b/src/core/courses/providers/course-link-handler.ts index 3dfb1fd39..1720cb9b6 100644 --- a/src/core/courses/providers/course-link-handler.ts +++ b/src/core/courses/providers/course-link-handler.ts @@ -16,10 +16,10 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; -import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; -import { CoreLoginHelperProvider } from '../../login/providers/helper'; -import { CoreCourseProvider } from '../../course/providers/course'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '@core/login/providers/helper'; +import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCoursesProvider } from './courses'; /** diff --git a/src/core/courses/providers/courses-index-link-handler.ts b/src/core/courses/providers/courses-index-link-handler.ts index ebf41c4cf..0183689df 100644 --- a/src/core/courses/providers/courses-index-link-handler.ts +++ b/src/core/courses/providers/courses-index-link-handler.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; -import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; -import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreCoursesProvider } from './courses'; /** diff --git a/src/core/courses/providers/mainmenu-handler.ts b/src/core/courses/providers/mainmenu-handler.ts index 6727ff832..17073d458 100644 --- a/src/core/courses/providers/mainmenu-handler.ts +++ b/src/core/courses/providers/mainmenu-handler.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreCoursesProvider } from './courses'; -import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/providers/delegate'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate'; import { CoreCoursesMyOverviewProvider } from '../providers/my-overview'; /** diff --git a/src/core/courses/providers/my-overview-link-handler.ts b/src/core/courses/providers/my-overview-link-handler.ts index 1622bc5e7..e35151615 100644 --- a/src/core/courses/providers/my-overview-link-handler.ts +++ b/src/core/courses/providers/my-overview-link-handler.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; -import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; -import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '@core/login/providers/helper'; /** * Handler to treat links to my overview. diff --git a/src/core/emulator/emulator.module.ts b/src/core/emulator/emulator.module.ts index 353369710..8229d4bec 100644 --- a/src/core/emulator/emulator.module.ts +++ b/src/core/emulator/emulator.module.ts @@ -56,6 +56,24 @@ import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreInitDelegate } from '@providers/init'; +// List of Ionic Native providers. +export const IONIC_NATIVE_PROVIDERS = [ + Camera, + Clipboard, + File, + FileTransfer, + Globalization, + InAppBrowser, + Keyboard, + LocalNotifications, + MediaCapture, + Network, + SplashScreen, + StatusBar, + SQLite, + Zip +]; + /** * This module handles the emulation of Cordova plugins in browser and desktop. * diff --git a/src/core/emulator/providers/local-notifications.ts b/src/core/emulator/providers/local-notifications.ts index 6e19d6474..f59120623 100644 --- a/src/core/emulator/providers/local-notifications.ts +++ b/src/core/emulator/providers/local-notifications.ts @@ -17,7 +17,7 @@ import { LocalNotifications, ILocalNotification } from '@ionic-native/local-noti import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { SQLiteDB } from '@classes/sqlitedb'; -import { CoreConstants } from '../../constants'; +import { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../../../configconstants'; import * as moment from 'moment'; diff --git a/src/core/fileuploader/fileuploader.module.ts b/src/core/fileuploader/fileuploader.module.ts index 5d52372bb..d0545d32e 100644 --- a/src/core/fileuploader/fileuploader.module.ts +++ b/src/core/fileuploader/fileuploader.module.ts @@ -22,21 +22,25 @@ import { CoreFileUploaderCameraHandler } from './providers/camera-handler'; import { CoreFileUploaderFileHandler } from './providers/file-handler'; import { CoreFileUploaderVideoHandler } from './providers/video-handler'; +// List of providers (without handlers). +export const CORE_FILEUPLOADER_PROVIDERS: any[] = [ + CoreFileUploaderProvider, + CoreFileUploaderHelperProvider, + CoreFileUploaderDelegate +]; + @NgModule({ declarations: [ ], imports: [ ], - providers: [ - CoreFileUploaderProvider, - CoreFileUploaderHelperProvider, - CoreFileUploaderDelegate, + providers: CORE_FILEUPLOADER_PROVIDERS.concat([ CoreFileUploaderAlbumHandler, CoreFileUploaderAudioHandler, CoreFileUploaderCameraHandler, CoreFileUploaderFileHandler, CoreFileUploaderVideoHandler - ] + ]) }) export class CoreFileUploaderModule { constructor(delegate: CoreFileUploaderDelegate, albumHandler: CoreFileUploaderAlbumHandler, diff --git a/src/core/grades/components/components.module.ts b/src/core/grades/components/components.module.ts index 872bb100f..8088706e7 100644 --- a/src/core/grades/components/components.module.ts +++ b/src/core/grades/components/components.module.ts @@ -18,8 +18,8 @@ import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreGradesCourseComponent } from './course/course'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; -import { CorePipesModule } from '@pipes'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; @NgModule({ declarations: [ diff --git a/src/core/grades/components/course/course.ts b/src/core/grades/components/course/course.ts index 181080f4d..4dc6e1ba3 100644 --- a/src/core/grades/components/course/course.ts +++ b/src/core/grades/components/course/course.ts @@ -39,8 +39,9 @@ export class CoreGradesCourseComponent { gradesTable: any; constructor(private gradesProvider: CoreGradesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, - private gradesHelper: CoreGradesHelperProvider, private sitesProvider: CoreSitesProvider, private navCtrl: NavController, - private appProvider: CoreAppProvider, @Optional() private svComponent: CoreSplitViewComponent) { + private gradesHelper: CoreGradesHelperProvider, private sitesProvider: CoreSitesProvider, + @Optional() private navCtrl: NavController, private appProvider: CoreAppProvider, + @Optional() private svComponent: CoreSplitViewComponent) { } /** diff --git a/src/core/grades/grades.module.ts b/src/core/grades/grades.module.ts index d090f2e00..d6f3d9889 100644 --- a/src/core/grades/grades.module.ts +++ b/src/core/grades/grades.module.ts @@ -15,19 +15,25 @@ import { NgModule } from '@angular/core'; import { CoreGradesProvider } from './providers/grades'; import { CoreGradesHelperProvider } from './providers/helper'; -import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; +import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; import { CoreGradesMainMenuHandler } from './providers/mainmenu-handler'; import { CoreGradesCourseOptionHandler } from './providers/course-option-handler'; import { CoreGradesComponentsModule } from './components/components.module'; -import { CoreCourseOptionsDelegate } from '../course/providers/options-delegate'; +import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreGradesUserLinkHandler } from './providers/user-link-handler'; import { CoreGradesOverviewLinkHandler } from './providers/overview-link-handler'; -import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreGradesUserHandler } from './providers/user-handler'; -import { CoreUserDelegate } from '../user/providers/user-delegate'; +import { CoreUserDelegate } from '@core/user/providers/user-delegate'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreUserProvider } from '../user/providers/user'; +import { CoreUserProvider } from '@core/user/providers/user'; + +// List of providers (without handlers). +export const CORE_GRADES_PROVIDERS: any[] = [ + CoreGradesProvider, + CoreGradesHelperProvider +]; @NgModule({ declarations: [ @@ -35,15 +41,13 @@ import { CoreUserProvider } from '../user/providers/user'; imports: [ CoreGradesComponentsModule ], - providers: [ - CoreGradesProvider, - CoreGradesHelperProvider, + providers: CORE_GRADES_PROVIDERS.concat([ CoreGradesMainMenuHandler, CoreGradesCourseOptionHandler, CoreGradesUserLinkHandler, CoreGradesOverviewLinkHandler, CoreGradesUserHandler - ] + ]) }) export class CoreGradesModule { constructor(mainMenuDelegate: CoreMainMenuDelegate, gradesMenuHandler: CoreGradesMainMenuHandler, diff --git a/src/core/grades/pages/courses/courses.module.ts b/src/core/grades/pages/courses/courses.module.ts index 05bb74eab..40339d8ff 100644 --- a/src/core/grades/pages/courses/courses.module.ts +++ b/src/core/grades/pages/courses/courses.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreGradesCoursesPage } from './courses'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/grades/pages/grade/grade.module.ts b/src/core/grades/pages/grade/grade.module.ts index 159a3c28e..6e6c55fa9 100644 --- a/src/core/grades/pages/grade/grade.module.ts +++ b/src/core/grades/pages/grade/grade.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreGradesGradePage } from './grade'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/grades/providers/course-option-handler.ts b/src/core/grades/providers/course-option-handler.ts index 54f9396e7..2a13d0bfe 100644 --- a/src/core/grades/providers/course-option-handler.ts +++ b/src/core/grades/providers/course-option-handler.ts @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController } from 'ionic-angular'; -import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '../../course/providers/options-delegate'; -import { CoreCourseProvider } from '../../course/providers/course'; +import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreGradesProvider } from './grades'; -import { CoreCoursesProvider } from '../../courses/providers/courses'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreGradesCourseComponent } from '../components/course/course'; /** @@ -80,9 +80,11 @@ export class CoreGradesCourseOptionHandler implements CoreCourseOptionsHandler { /** * Returns the data needed to render the handler. * - * @return {CoreCourseOptionsHandlerData} Data needed to render the handler. + * @param {Injector} injector Injector. + * @param {number} courseId The course ID. + * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData(): CoreCourseOptionsHandlerData { + getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { return { title: 'core.grades.grades', class: 'core-grades-course-handler', diff --git a/src/core/grades/providers/grades.ts b/src/core/grades/providers/grades.ts index 15f21e1b4..241ca14fb 100644 --- a/src/core/grades/providers/grades.ts +++ b/src/core/grades/providers/grades.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSite } from '@classes/site'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreCoursesProvider } from '../../courses/providers/courses'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; /** * Service to provide grade functionalities. diff --git a/src/core/grades/providers/helper.ts b/src/core/grades/providers/helper.ts index ceffae450..ef98334ed 100644 --- a/src/core/grades/providers/helper.ts +++ b/src/core/grades/providers/helper.ts @@ -16,8 +16,8 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { TranslateService } from '@ngx-translate/core'; -import { CoreCoursesProvider } from '../../courses/providers/courses'; -import { CoreCourseProvider } from '../../course/providers/course'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreGradesProvider } from './grades'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; diff --git a/src/core/grades/providers/mainmenu-handler.ts b/src/core/grades/providers/mainmenu-handler.ts index 59c2b016e..fe3b96a77 100644 --- a/src/core/grades/providers/mainmenu-handler.ts +++ b/src/core/grades/providers/mainmenu-handler.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreGradesProvider } from './grades'; -import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/providers/delegate'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate'; /** * Handler to inject an option into main menu. diff --git a/src/core/grades/providers/overview-link-handler.ts b/src/core/grades/providers/overview-link-handler.ts index 915b9e2c2..32ab28ac1 100644 --- a/src/core/grades/providers/overview-link-handler.ts +++ b/src/core/grades/providers/overview-link-handler.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; -import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; -import { CoreContentLinksHelperProvider } from '../../contentlinks/providers/helper'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreGradesProvider } from './grades'; /** diff --git a/src/core/grades/providers/user-handler.ts b/src/core/grades/providers/user-handler.ts index ba95dc857..b7f5f42bf 100644 --- a/src/core/grades/providers/user-handler.ts +++ b/src/core/grades/providers/user-handler.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '../../user/providers/user-delegate'; +import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreContentLinksHelperProvider } from '../../contentlinks/providers/helper'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreGradesProvider } from './grades'; /** diff --git a/src/core/grades/providers/user-link-handler.ts b/src/core/grades/providers/user-link-handler.ts index 589f258aa..91e0d4f19 100644 --- a/src/core/grades/providers/user-link-handler.ts +++ b/src/core/grades/providers/user-link-handler.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; -import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; -import { CoreContentLinksHelperProvider } from '../../contentlinks/providers/helper'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreGradesProvider } from './grades'; /** diff --git a/src/core/login/login.module.ts b/src/core/login/login.module.ts index 8e4f9a3d8..55d40cff9 100644 --- a/src/core/login/login.module.ts +++ b/src/core/login/login.module.ts @@ -15,13 +15,16 @@ import { NgModule } from '@angular/core'; import { CoreLoginHelperProvider } from './providers/helper'; +// List of providers. +export const CORE_LOGIN_PROVIDERS = [ + CoreLoginHelperProvider +]; + @NgModule({ declarations: [ ], imports: [ ], - providers: [ - CoreLoginHelperProvider - ] + providers: CORE_LOGIN_PROVIDERS }) export class CoreLoginModule {} diff --git a/src/core/login/pages/credentials/credentials.module.ts b/src/core/login/pages/credentials/credentials.module.ts index a5b767d10..98918503d 100644 --- a/src/core/login/pages/credentials/credentials.module.ts +++ b/src/core/login/pages/credentials/credentials.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { CoreLoginCredentialsPage } from './credentials'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts index 75ffd2b0b..b80ee030e 100644 --- a/src/core/login/pages/credentials/credentials.ts +++ b/src/core/login/pages/credentials/credentials.ts @@ -21,8 +21,8 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreLoginHelperProvider } from '../../providers/helper'; -import { CoreContentLinksDelegate } from '../../../contentlinks/providers/delegate'; -import { CoreContentLinksHelperProvider } from '../../../contentlinks/providers/helper'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; /** diff --git a/src/core/login/pages/email-signup/email-signup.module.ts b/src/core/login/pages/email-signup/email-signup.module.ts index cf128a353..c0db4ac71 100644 --- a/src/core/login/pages/email-signup/email-signup.module.ts +++ b/src/core/login/pages/email-signup/email-signup.module.ts @@ -17,8 +17,8 @@ import { IonicPageModule } from 'ionic-angular'; import { CoreLoginEmailSignupPage } from './email-signup'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; -import { CoreUserComponentsModule } from '../../../user/components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreUserComponentsModule } from '@core/user/components/components.module'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/email-signup/email-signup.ts b/src/core/login/pages/email-signup/email-signup.ts index 606178f13..b713caab5 100644 --- a/src/core/login/pages/email-signup/email-signup.ts +++ b/src/core/login/pages/email-signup/email-signup.ts @@ -22,7 +22,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreWSProvider } from '@providers/ws'; import { CoreLoginHelperProvider } from '../../providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { CoreUserProfileFieldDelegate } from '../../../user/providers/user-profile-field-delegate'; +import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; /** * Page to signup using email. diff --git a/src/core/login/pages/reconnect/reconnect.module.ts b/src/core/login/pages/reconnect/reconnect.module.ts index 40f59485f..412de6c45 100644 --- a/src/core/login/pages/reconnect/reconnect.module.ts +++ b/src/core/login/pages/reconnect/reconnect.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreLoginReconnectPage } from './reconnect'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/reconnect/reconnect.ts b/src/core/login/pages/reconnect/reconnect.ts index 1dc684011..99e1ad423 100644 --- a/src/core/login/pages/reconnect/reconnect.ts +++ b/src/core/login/pages/reconnect/reconnect.ts @@ -88,6 +88,8 @@ export class CoreLoginReconnectPage { return site.getPublicConfig().then((config) => { this.logoUrl = config.logourl || config.compactlogourl; + }).catch(() => { + // Ignore errors. }); } }).catch(() => { diff --git a/src/core/login/pages/site-error/site-error.module.ts b/src/core/login/pages/site-error/site-error.module.ts index 60284f65f..a7276f0a7 100644 --- a/src/core/login/pages/site-error/site-error.module.ts +++ b/src/core/login/pages/site-error/site-error.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreLoginSiteErrorPage } from './site-error'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/site-help/site-help.module.ts b/src/core/login/pages/site-help/site-help.module.ts index c9f01d134..8aa07277d 100644 --- a/src/core/login/pages/site-help/site-help.module.ts +++ b/src/core/login/pages/site-help/site-help.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreLoginSiteHelpPage } from './site-help'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/site-policy/site-policy.module.ts b/src/core/login/pages/site-policy/site-policy.module.ts index 65475306b..21e868074 100644 --- a/src/core/login/pages/site-policy/site-policy.module.ts +++ b/src/core/login/pages/site-policy/site-policy.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { CoreLoginSitePolicyPage } from './site-policy'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/site/site.module.ts b/src/core/login/pages/site/site.module.ts index a188fdfc9..230f60cd6 100644 --- a/src/core/login/pages/site/site.module.ts +++ b/src/core/login/pages/site/site.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreLoginSitePage } from './site'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/sites/sites.module.ts b/src/core/login/pages/sites/sites.module.ts index 5bf8716fd..72b515f38 100644 --- a/src/core/login/pages/sites/sites.module.ts +++ b/src/core/login/pages/sites/sites.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreLoginSitesPage } from './sites'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index d75c7f3e5..482a7b356 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -26,8 +26,9 @@ 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 { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins'; import { CoreConfigConstants } from '../../../configconstants'; -import { CoreConstants } from '../../constants'; +import { CoreConstants } from '@core/constants'; import { Md5 } from 'ts-md5/dist/md5'; /** @@ -80,7 +81,7 @@ export class CoreLoginHelperProvider { private wsProvider: CoreWSProvider, private translate: TranslateService, private textUtils: CoreTextUtilsProvider, private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private configProvider: CoreConfigProvider, private platform: Platform, - private initDelegate: CoreInitDelegate) { + private initDelegate: CoreInitDelegate, private sitePluginsProvider: CoreSitePluginsProvider) { this.logger = logger.getInstance('CoreLoginHelper'); } @@ -768,10 +769,15 @@ export class CoreLoginHelperProvider { if (this.sitesProvider.isLoggedIn()) { if (siteId && siteId != this.sitesProvider.getCurrentSiteId()) { // Target page belongs to a different site. Change site. - // @todo Store redirect once we have addon manager. - this.sitesProvider.logout().then(() => { - this.loadSiteAndPage(page, params, siteId); - }); + if (this.sitePluginsProvider.hasSitePluginsLoaded) { + // The site has site plugins so the app will be restarted. Store the data and logout. + this.appProvider.storeRedirect(siteId, page, params); + this.sitesProvider.logout(); + } else { + this.sitesProvider.logout().then(() => { + this.loadSiteAndPage(page, params, siteId); + }); + } } else { this.loadPageInMainMenu(page, params); } @@ -936,9 +942,8 @@ export class CoreLoginHelperProvider { const params = url.split(':::'); return this.configProvider.get(CoreConstants.LOGIN_LAUNCH_DATA).then((data): any => { - try { - data = JSON.parse(data); - } catch (ex) { + data = this.textUtils.parseJSON(data, null); + if (data === null) { return Promise.reject(null); } diff --git a/src/core/mainmenu/mainmenu.module.ts b/src/core/mainmenu/mainmenu.module.ts index 0a49f84b8..7873056cb 100644 --- a/src/core/mainmenu/mainmenu.module.ts +++ b/src/core/mainmenu/mainmenu.module.ts @@ -16,14 +16,17 @@ import { NgModule } from '@angular/core'; import { CoreMainMenuDelegate } from './providers/delegate'; import { CoreMainMenuProvider } from './providers/mainmenu'; +// List of providers. +export const CORE_MAINMENU_PROVIDERS = [ + CoreMainMenuDelegate, + CoreMainMenuProvider +]; + @NgModule({ declarations: [ ], imports: [ ], - providers: [ - CoreMainMenuDelegate, - CoreMainMenuProvider - ] + providers: CORE_MAINMENU_PROVIDERS }) export class CoreMainMenuModule {} diff --git a/src/core/mainmenu/pages/menu/menu.html b/src/core/mainmenu/pages/menu/menu.html index f053d132d..54839b8ee 100644 --- a/src/core/mainmenu/pages/menu/menu.html +++ b/src/core/mainmenu/pages/menu/menu.html @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/src/core/mainmenu/pages/menu/menu.ts b/src/core/mainmenu/pages/menu/menu.ts index acd3a99eb..d909aa13a 100644 --- a/src/core/mainmenu/pages/menu/menu.ts +++ b/src/core/mainmenu/pages/menu/menu.ts @@ -77,7 +77,7 @@ export class CoreMainMenuPage implements OnDestroy { */ ionViewDidLoad(): void { if (!this.sitesProvider.isLoggedIn()) { - this.navCtrl.setRoot('CoreLoginSitesPage'); + this.navCtrl.setRoot('CoreLoginInitPage'); return; } diff --git a/src/core/mainmenu/pages/more/more.module.ts b/src/core/mainmenu/pages/more/more.module.ts index 0f2e6b72c..0c5705079 100644 --- a/src/core/mainmenu/pages/more/more.module.ts +++ b/src/core/mainmenu/pages/more/more.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreMainMenuMorePage } from './more'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/mainmenu/providers/delegate.ts b/src/core/mainmenu/providers/delegate.ts index 5d8b64865..ef87d35f3 100644 --- a/src/core/mainmenu/providers/delegate.ts +++ b/src/core/mainmenu/providers/delegate.ts @@ -82,6 +82,12 @@ export interface CoreMainMenuHandlerData { * @type {boolean} */ loading?: boolean; + + /** + * Params to pass to the page. + * @type {any} + */ + pageParams?: any; } /** diff --git a/src/core/settings/pages/list/list.module.ts b/src/core/settings/pages/list/list.module.ts index 5c2fcaf3a..56cc21396 100644 --- a/src/core/settings/pages/list/list.module.ts +++ b/src/core/settings/pages/list/list.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreSettingsListPage } from './list'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/sharedfiles/pages/choose-site/choose-site.module.ts b/src/core/sharedfiles/pages/choose-site/choose-site.module.ts index 4acb401d6..4711cc588 100644 --- a/src/core/sharedfiles/pages/choose-site/choose-site.module.ts +++ b/src/core/sharedfiles/pages/choose-site/choose-site.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { CoreSharedFilesChooseSitePage } from './choose-site'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/sharedfiles/pages/list/list.module.ts b/src/core/sharedfiles/pages/list/list.module.ts index 9fbe43b58..2211d812f 100644 --- a/src/core/sharedfiles/pages/list/list.module.ts +++ b/src/core/sharedfiles/pages/list/list.module.ts @@ -17,7 +17,7 @@ import { IonicPageModule } from 'ionic-angular'; import { CoreSharedFilesListPage } from './list'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/sharedfiles/providers/helper.ts b/src/core/sharedfiles/providers/helper.ts index 36cbcbba7..a3a4341e3 100644 --- a/src/core/sharedfiles/providers/helper.ts +++ b/src/core/sharedfiles/providers/helper.ts @@ -23,7 +23,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSharedFilesProvider } from './sharedfiles'; -import { CoreFileUploaderProvider } from '../../fileuploader/providers/fileuploader'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; /** * Helper service to share files with the app. diff --git a/src/core/sharedfiles/providers/upload-handler.ts b/src/core/sharedfiles/providers/upload-handler.ts index abb4331de..da602b2b3 100644 --- a/src/core/sharedfiles/providers/upload-handler.ts +++ b/src/core/sharedfiles/providers/upload-handler.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { Platform } from 'ionic-angular'; -import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from '../../fileuploader/providers/delegate'; +import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from '@core/fileuploader/providers/delegate'; import { CoreSharedFilesHelperProvider } from './helper'; /** * Handler to upload files from the album. diff --git a/src/core/sharedfiles/sharedfiles.module.ts b/src/core/sharedfiles/sharedfiles.module.ts index e58bbd904..dcb47ad1c 100644 --- a/src/core/sharedfiles/sharedfiles.module.ts +++ b/src/core/sharedfiles/sharedfiles.module.ts @@ -17,18 +17,22 @@ import { Platform } from 'ionic-angular'; import { CoreSharedFilesProvider } from './providers/sharedfiles'; import { CoreSharedFilesHelperProvider } from './providers/helper'; import { CoreSharedFilesUploadHandler } from './providers/upload-handler'; -import { CoreFileUploaderDelegate } from '../fileuploader/providers/delegate'; +import { CoreFileUploaderDelegate } from '@core/fileuploader/providers/delegate'; + +// List of providers (without handlers). +export const CORE_SHAREDFILES_PROVIDERS: any[] = [ + CoreSharedFilesProvider, + CoreSharedFilesHelperProvider +]; @NgModule({ declarations: [ ], imports: [ ], - providers: [ - CoreSharedFilesProvider, - CoreSharedFilesHelperProvider, + providers: CORE_SHAREDFILES_PROVIDERS.concat([ CoreSharedFilesUploadHandler - ] + ]) }) export class CoreSharedFilesModule { constructor(platform: Platform, delegate: CoreFileUploaderDelegate, handler: CoreSharedFilesUploadHandler, diff --git a/src/core/sitehome/components/all-course-list/all-course-list.ts b/src/core/sitehome/components/all-course-list/all-course-list.ts index 3e71c3373..4d8785918 100644 --- a/src/core/sitehome/components/all-course-list/all-course-list.ts +++ b/src/core/sitehome/components/all-course-list/all-course-list.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component } from '@angular/core'; -import { CoreCoursesProvider } from '../../../courses/providers/courses'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; /** * Component to open the page to view the list of all courses. diff --git a/src/core/sitehome/components/categories/categories.ts b/src/core/sitehome/components/categories/categories.ts index 3ae52da32..9bd62f2f0 100644 --- a/src/core/sitehome/components/categories/categories.ts +++ b/src/core/sitehome/components/categories/categories.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component } from '@angular/core'; -import { CoreCoursesProvider } from '../../../courses/providers/courses'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; /** * Component to open the page to view the list of categories. diff --git a/src/core/sitehome/components/components.module.ts b/src/core/sitehome/components/components.module.ts index 9a2a04469..d0fd2ddf8 100644 --- a/src/core/sitehome/components/components.module.ts +++ b/src/core/sitehome/components/components.module.ts @@ -17,8 +17,8 @@ import { CommonModule } from '@angular/common'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; -import { CoreCourseComponentsModule } from '../../course/components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { CoreSiteHomeIndexComponent } from './index/index'; import { CoreSiteHomeAllCourseListComponent } from './all-course-list/all-course-list'; import { CoreSiteHomeCategoriesComponent } from './categories/categories'; diff --git a/src/core/sitehome/components/course-search/course-search.ts b/src/core/sitehome/components/course-search/course-search.ts index 454a3d1f4..558d53ee8 100644 --- a/src/core/sitehome/components/course-search/course-search.ts +++ b/src/core/sitehome/components/course-search/course-search.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component } from '@angular/core'; -import { CoreCoursesProvider } from '../../../courses/providers/courses'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; /** * Component to open the page to search courses. diff --git a/src/core/sitehome/components/enrolled-course-list/enrolled-course-list.ts b/src/core/sitehome/components/enrolled-course-list/enrolled-course-list.ts index 4c53ab4ff..efe66f205 100644 --- a/src/core/sitehome/components/enrolled-course-list/enrolled-course-list.ts +++ b/src/core/sitehome/components/enrolled-course-list/enrolled-course-list.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit } from '@angular/core'; -import { CoreCoursesProvider } from '../../../courses/providers/courses'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; /** * Component to open the page to view the list of courses the user is enrolled in. diff --git a/src/core/sitehome/components/index/index.ts b/src/core/sitehome/components/index/index.ts index 2c6140efb..27b414e28 100644 --- a/src/core/sitehome/components/index/index.ts +++ b/src/core/sitehome/components/index/index.ts @@ -15,9 +15,9 @@ import { Component, OnInit } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreCourseProvider } from '../../../course/providers/course'; -import { CoreCourseHelperProvider } from '../../../course/providers/helper'; -import { CoreCourseModulePrefetchDelegate } from '../../../course/providers/module-prefetch-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; /** * Component that displays site home index. diff --git a/src/core/sitehome/pages/index/index.ts b/src/core/sitehome/pages/index/index.ts index 162ca1561..4262af9e1 100644 --- a/src/core/sitehome/pages/index/index.ts +++ b/src/core/sitehome/pages/index/index.ts @@ -15,7 +15,7 @@ import { Component } from '@angular/core'; import { IonicPage, NavParams, NavController } from 'ionic-angular'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreCourseHelperProvider } from '../../../course/providers/helper'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; /** * Page that displays site home index. diff --git a/src/core/sitehome/providers/index-link-handler.ts b/src/core/sitehome/providers/index-link-handler.ts index 12ec609cb..d8d5c5c64 100644 --- a/src/core/sitehome/providers/index-link-handler.ts +++ b/src/core/sitehome/providers/index-link-handler.ts @@ -14,9 +14,9 @@ import { Injectable } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; -import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; -import { CoreLoginHelperProvider } from '../../login/providers/helper'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreSiteHomeProvider } from './sitehome'; /** diff --git a/src/core/sitehome/providers/mainmenu-handler.ts b/src/core/sitehome/providers/mainmenu-handler.ts index c2cb016b2..56cd67156 100644 --- a/src/core/sitehome/providers/mainmenu-handler.ts +++ b/src/core/sitehome/providers/mainmenu-handler.ts @@ -14,8 +14,8 @@ import { Injectable } from '@angular/core'; import { CoreSiteHomeProvider } from './sitehome'; -import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/providers/delegate'; -import { CoreCoursesMyOverviewProvider } from '../../courses/providers/my-overview'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate'; +import { CoreCoursesMyOverviewProvider } from '@core/courses/providers/my-overview'; /** * Handler to add Site Home into main menu. diff --git a/src/core/sitehome/providers/sitehome.ts b/src/core/sitehome/providers/sitehome.ts index 036828a8a..de8957226 100644 --- a/src/core/sitehome/providers/sitehome.ts +++ b/src/core/sitehome/providers/sitehome.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSite } from '@classes/site'; -import { CoreCourseProvider } from '../../course/providers/course'; +import { CoreCourseProvider } from '@core/course/providers/course'; /** * Service that provides some features regarding site home. diff --git a/src/core/sitehome/sitehome.module.ts b/src/core/sitehome/sitehome.module.ts index c498d2b12..303859c7d 100644 --- a/src/core/sitehome/sitehome.module.ts +++ b/src/core/sitehome/sitehome.module.ts @@ -16,18 +16,22 @@ import { NgModule } from '@angular/core'; import { CoreSiteHomeProvider } from './providers/sitehome'; import { CoreSiteHomeMainMenuHandler } from './providers/mainmenu-handler'; import { CoreSiteHomeIndexLinkHandler } from './providers/index-link-handler'; -import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; -import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; +import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; + +// List of providers (without handlers). +export const CORE_SITEHOME_PROVIDERS: any[] = [ + CoreSiteHomeProvider +]; @NgModule({ declarations: [], imports: [ ], - providers: [ - CoreSiteHomeProvider, + providers: CORE_SITEHOME_PROVIDERS.concat([ CoreSiteHomeMainMenuHandler, CoreSiteHomeIndexLinkHandler - ], + ]), exports: [] }) export class CoreSiteHomeModule { diff --git a/src/core/siteplugins/classes/base-handler.ts b/src/core/siteplugins/classes/base-handler.ts new file mode 100644 index 000000000..7226a0202 --- /dev/null +++ b/src/core/siteplugins/classes/base-handler.ts @@ -0,0 +1,32 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreDelegateHandler } from '@classes/delegate'; + +/** + * Super class for handlers for site plugins. + */ +export class CoreSitePluginsBaseHandler implements CoreDelegateHandler { + + constructor(public name: string) { } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/core/siteplugins/classes/call-ws-click-directive.ts b/src/core/siteplugins/classes/call-ws-click-directive.ts new file mode 100644 index 000000000..7f535eaad --- /dev/null +++ b/src/core/siteplugins/classes/call-ws-click-directive.ts @@ -0,0 +1,73 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Input, OnInit, ElementRef } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; +import { CoreSitePluginsCallWSBaseDirective } from './call-ws-directive'; + +/** + * Base class for directives to call a WS when the element is clicked. + * + * The directives that inherit from this class will call a WS method when the element is clicked. + * + * @see CoreSitePluginsCallWSBaseDirective + */ +export class CoreSitePluginsCallWSOnClickBaseDirective extends CoreSitePluginsCallWSBaseDirective implements OnInit { + @Input() confirmMessage: string; // Message to confirm the action. If not supplied, no confirmation. If empty, default message. + + constructor(element: ElementRef, protected translate: TranslateService, protected domUtils: CoreDomUtilsProvider, + protected sitePluginsProvider: CoreSitePluginsProvider, + protected parentContent: CoreSitePluginsPluginContentComponent) { + super(element, translate, domUtils, sitePluginsProvider, parentContent); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + this.element.addEventListener('click', (ev: Event): void => { + ev.preventDefault(); + ev.stopPropagation(); + + if (typeof this.confirmMessage != 'undefined') { + // Ask for confirm. + this.domUtils.showConfirm(this.confirmMessage || this.translate.instant('core.areyousure')).then(() => { + this.callWS(); + }).catch(() => { + // User cancelled, ignore. + }); + } else { + this.callWS(); + } + }); + } + + /** + * Call a WS. + * + * @return {Promise} Promise resolved when done. + */ + protected callWS(): Promise { + const modal = this.domUtils.showModalLoading(); + + return super.callWS().finally(() => { + modal.dismiss(); + }); + } +} diff --git a/src/core/siteplugins/classes/call-ws-directive.ts b/src/core/siteplugins/classes/call-ws-directive.ts new file mode 100644 index 000000000..8d7dedc4b --- /dev/null +++ b/src/core/siteplugins/classes/call-ws-directive.ts @@ -0,0 +1,114 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Input, OnInit, OnDestroy, ElementRef } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; +import { Subscription } from 'rxjs'; + +/** + * Base class for directives that need to call a WS. + */ +export class CoreSitePluginsCallWSBaseDirective implements OnInit, OnDestroy { + @Input() name: string; // The name of the WS to call. + @Input() params: any; // The params for the WS call. + @Input() preSets: any; // The preSets for the WS call. + @Input() useOtherDataForWS: any[]; // Whether to include other data in the params for the WS. + // @see CoreSitePluginsProvider.loadOtherDataInArgs. + @Input() form: string; // ID or name to identify a form. The form will be obtained from document.forms. + // If supplied and form is found, the form data will be retrieved and sent to the WS. + + protected element: HTMLElement; + protected invalidateObserver: Subscription; + + constructor(element: ElementRef, protected translate: TranslateService, protected domUtils: CoreDomUtilsProvider, + protected sitePluginsProvider: CoreSitePluginsProvider, + protected parentContent: CoreSitePluginsPluginContentComponent) { + this.element = element.nativeElement || element; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + if (this.parentContent && this.parentContent.invalidateObservable) { + this.invalidateObserver = this.parentContent.invalidateObservable.subscribe(() => { + this.invalidate(); + }); + } + } + + /** + * Call a WS. + * + * @return {Promise} Promise resolved when done. + */ + protected callWS(): Promise { + const params = this.getParamsForWS(); + + return this.sitePluginsProvider.callWS(this.name, params, this.preSets).then((result) => { + return this.wsCallSuccess(result); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.serverconnection', true); + }); + } + + /** + * Get the params for the WS call. + * + * @return {any} Params. + */ + protected getParamsForWS(): any { + let params = this.params || {}; + + if (this.parentContent) { + params = this.sitePluginsProvider.loadOtherDataInArgs(params, this.parentContent.otherData, this.useOtherDataForWS); + } + + if (this.form && document.forms[this.form]) { + params = Object.assign(params, this.domUtils.getDataFromForm(document.forms[this.form])); + } + + return params; + } + + /** + * Function called when the WS call is successful. + * + * @param {any} result Result of the WS call. + */ + protected wsCallSuccess(result: any): void { + // Function to be overridden. + } + + /** + * Invalidate the WS call. + * + * @return {Promise} Promise resolved when done. + */ + invalidate(): Promise { + const params = this.getParamsForWS(); + + return this.sitePluginsProvider.invalidateCallWS(this.name, params, this.preSets); + } + + /** + * Directive destroyed. + */ + ngOnDestroy(): void { + this.invalidateObserver && this.invalidateObserver.unsubscribe(); + } +} diff --git a/src/core/siteplugins/classes/course-format-handler.ts b/src/core/siteplugins/classes/course-format-handler.ts new file mode 100644 index 000000000..91e3a5366 --- /dev/null +++ b/src/core/siteplugins/classes/course-format-handler.ts @@ -0,0 +1,74 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Injector } from '@angular/core'; +import { CoreCourseFormatHandler } from '@core/course/providers/format-delegate'; +import { CoreSitePluginsBaseHandler } from './base-handler'; +import { CoreSitePluginsCourseFormatComponent } from '../components/course-format/course-format'; + +/** + * Handler to support a course format using a site plugin. + */ +export class CoreSitePluginsCourseFormatHandler extends CoreSitePluginsBaseHandler implements CoreCourseFormatHandler { + + constructor(name: string, protected handlerSchema: any) { + super(name); + } + + /** + * Whether it allows seeing all sections at the same time. Defaults to true. + * + * @param {any} course The course to check. + * @type {boolean} Whether it can view all sections. + */ + canViewAllSections(course: any): boolean { + return typeof this.handlerSchema.canviewallsections != 'undefined' ? this.handlerSchema.canviewallsections : true; + } + + /** + * Whether the option to enable section/module download should be displayed. Defaults to true. + * + * @param {any} course The course to check. + * @type {boolean} Whether the option to enable section/module download should be displayed. + */ + displayEnableDownload(course: any): boolean { + return typeof this.handlerSchema.displayenabledownload != 'undefined' ? this.handlerSchema.displayenabledownload : true; + } + + /** + * Whether the default section selector should be displayed. Defaults to true. + * + * @param {any} course The course to check. + * @type {boolean} Whether the default section selector should be displayed. + */ + displaySectionSelector(course: any): boolean { + return typeof this.handlerSchema.displaysectionselector != 'undefined' ? this.handlerSchema.displaysectionselector : true; + } + + /** + * Return the Component to use to display the course format instead of using the default one. + * Use it if you want to display a format completely different from the default one. + * If you want to customize the default format there are several methods to customize parts of it. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} course The course to render. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getCourseFormatComponent(injector: Injector, course: any): any | Promise { + if (this.handlerSchema.method) { + return CoreSitePluginsCourseFormatComponent; + } + } +} diff --git a/src/core/siteplugins/classes/course-option-handler.ts b/src/core/siteplugins/classes/course-option-handler.ts new file mode 100644 index 000000000..c54f049a3 --- /dev/null +++ b/src/core/siteplugins/classes/course-option-handler.ts @@ -0,0 +1,80 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Injector } from '@angular/core'; +import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; +import { CoreSitePluginsBaseHandler } from './base-handler'; +import { CoreSitePluginsCourseOptionComponent } from '../components/course-option/course-option'; + +/** + * Handler to display a site plugin in course options. + */ +export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandler implements CoreCourseOptionsHandler { + priority: number; + + constructor(name: string, protected title: string, protected plugin: any, protected handlerSchema: any, + protected bootstrapResult: any, protected sitePluginsProvider: CoreSitePluginsProvider) { + super(name); + + this.priority = handlerSchema.priority; + } + + /** + * Whether or not the handler is enabled for a certain course. + * + * @param {number} courseId The course ID. + * @param {any} accessData Access type and data. Default, guest, ... + * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise { + return this.sitePluginsProvider.isHandlerEnabledForCourse( + courseId, this.handlerSchema.restricttoenrolledcourses, this.bootstrapResult.restrict); + } + + /** + * Returns the data needed to render the handler. + * + * @param {Injector} injector Injector. + * @param {number} courseId The course ID. + * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { + return { + title: this.title, + class: this.handlerSchema.displaydata.class, + component: CoreSitePluginsCourseOptionComponent, + componentData: { + handlerUniqueName: this.name + } + }; + } + + /** + * Called when a course is downloaded. It should prefetch all the data to be able to see the plugin in offline. + * + * @param {any} course The course. + * @return {Promise} Promise resolved when done. + */ + prefetch(course: any): Promise { + const args = { + courseid: course.id, + }, + component = this.plugin.component; + + return this.sitePluginsProvider.prefetchFunctions(component, args, this.handlerSchema, course.id, undefined, true); + } +} diff --git a/src/core/siteplugins/classes/main-menu-handler.ts b/src/core/siteplugins/classes/main-menu-handler.ts new file mode 100644 index 000000000..7be3245ac --- /dev/null +++ b/src/core/siteplugins/classes/main-menu-handler.ts @@ -0,0 +1,50 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to display a site plugin in the main menu. + */ +export class CoreSitePluginsMainMenuHandler extends CoreSitePluginsBaseHandler implements CoreMainMenuHandler { + priority: number; + + constructor(name: string, protected title: string, protected plugin: any, protected handlerSchema: any, + protected bootstrapResult: any) { + super(name); + + this.priority = handlerSchema.priority; + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreMainMenuHandlerData} Data. + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + title: this.title, + icon: this.handlerSchema.displaydata.icon, + class: this.handlerSchema.displaydata.class, + page: 'CoreSitePluginsPluginPage', + pageParams: { + title: this.title, + component: this.plugin.component, + method: this.handlerSchema.method, + bootstrapResult: this.bootstrapResult + } + }; + } +} diff --git a/src/core/siteplugins/classes/module-handler.ts b/src/core/siteplugins/classes/module-handler.ts new file mode 100644 index 000000000..69ec55ba8 --- /dev/null +++ b/src/core/siteplugins/classes/module-handler.ts @@ -0,0 +1,74 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Injector } from '@angular/core'; +import { NavController, NavOptions } from 'ionic-angular'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; +import { CoreSitePluginsBaseHandler } from './base-handler'; +import { CoreSitePluginsModuleIndexComponent } from '../components/module-index/module-index'; + +/** + * Handler to support a module using a site plugin. + */ +export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler implements CoreCourseModuleHandler { + priority: number; + + constructor(name: string, protected handlerSchema: any) { + super(name); + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param {any} module The module object. + * @param {number} courseId The course ID. + * @param {number} sectionId The section ID. + * @return {CoreCourseModuleHandlerData} Data to render the module. + */ + getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { + const hasOffline = !!(this.handlerSchema.offlinefunctions && Object.keys(this.handlerSchema.offlinefunctions).length), + showDowloadButton = this.handlerSchema.downloadbutton; + + return { + title: module.name, + icon: this.handlerSchema.displaydata.icon, + class: this.handlerSchema.displaydata.class, + showDownloadButton: typeof showDowloadButton != 'undefined' ? showDowloadButton : hasOffline, + action: (event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void => { + event.preventDefault(); + event.stopPropagation(); + + navCtrl.push('CoreSitePluginsModuleIndexPage', { + title: module.name, + module: module, + courseId: courseId + }, options); + } + }; + } + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * The component returned must implement CoreCourseModuleMainComponent. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} course The course object. + * @param {any} module The module object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getMainComponent(injector: Injector, course: any, module: any): any | Promise { + return CoreSitePluginsModuleIndexComponent; + } +} diff --git a/src/core/siteplugins/classes/module-prefetch-handler.ts b/src/core/siteplugins/classes/module-prefetch-handler.ts new file mode 100644 index 000000000..e309067e6 --- /dev/null +++ b/src/core/siteplugins/classes/module-prefetch-handler.ts @@ -0,0 +1,171 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Injector } from '@angular/core'; +import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; + +/** + * Handler to prefetch a module site plugin. + */ +export class CoreSitePluginsModulePrefetchHandler extends CoreCourseModulePrefetchHandlerBase { + protected ROOT_CACHE_KEY = 'CoreSitePluginsModulePrefetchHandler:'; + + constructor(injector: Injector, protected sitePluginsProvider: CoreSitePluginsProvider, component: string, modName: string, + protected handlerSchema: any) { + super(injector); + + this.component = component; + this.name = modName; + this.isResource = handlerSchema.isresource; + + if (handlerSchema.updatesnames) { + try { + this.updatesNames = new RegExp(handlerSchema.updatesnames); + } catch (ex) { + // Ignore errors. + } + } + } + + /** + * Download or prefetch the content. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {boolean} [prefetch] True to prefetch, false to download right away. + * @param {string} [dirPath] Path of the directory where to store all the content files. This is to keep the files + * relative paths and make the package work in an iframe. Undefined to download the files + * in the filepool root folder. + * @return {Promise} Promise resolved when all content is downloaded. Data returned is not reliable. + */ + downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise { + return this.prefetchPackage(module, courseId, false, this.downloadPrefetchPlugin.bind(this), undefined, prefetch, dirPath); + } + + /** + * Download or prefetch the plugin, downloading the files and calling the needed WS. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [prefetch] True to prefetch, false to download right away. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when done. + */ + protected downloadPrefetchPlugin(module: any, courseId: number, single?: boolean, siteId?: string, prefetch?: boolean, + dirPath?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + + const promises = [], + args = { + courseid: courseId, + cmid: module.id, + userid: site.getUserId() + }; + + // Download the files (if any). + promises.push(this.downloadOrPrefetchFiles(site.id, module, courseId, prefetch, dirPath)); + + // Call all the offline functions. + promises.push(this.sitePluginsProvider.prefetchFunctions(this.component, args, this.handlerSchema, courseId, + module, prefetch, dirPath, site)); + + return Promise.all(promises); + }); + } + + /** + * Download or prefetch the plugin files. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {boolean} [prefetch] True to prefetch, false to download right away. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when done. + */ + protected downloadOrPrefetchFiles(siteId: string, module: any, courseId: number, prefetch?: boolean, dirPath?: string) + : Promise { + // Load module contents (ignore cache so we always have the latest data). + return this.loadContents(module, courseId, true).then(() => { + // Get the intro files. + return this.getIntroFiles(module, courseId); + }).then((introFiles) => { + const contentFiles = this.getContentDownloadableFiles(module), + promises = []; + + if (dirPath) { + // Download intro files in filepool root folder. + promises.push(this.filepoolProvider.downloadOrPrefetchFiles(siteId, introFiles, prefetch, false, + this.component, module.id)); + + // Download content files inside dirPath. + promises.push(this.filepoolProvider.downloadOrPrefetchFiles(siteId, contentFiles, prefetch, false, + this.component, module.id, dirPath)); + } else { + // No dirPath, download everything in filepool root folder. + const files = introFiles.concat(contentFiles); + promises.push(this.filepoolProvider.downloadOrPrefetchFiles(siteId, files, prefetch, false, + this.component, module.id)); + } + + return Promise.all(promises); + }); + } + + /** + * Invalidate the prefetched content. + * + * @param {number} moduleId The module ID. + * @param {number} courseId Course ID the module belongs to. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + const promises = [], + currentSite = this.sitesProvider.getCurrentSite(), + siteId = currentSite.getId(), + args = { + courseid: courseId, + cmid: moduleId, + userid: currentSite.getUserId() + }; + + // Invalidate files and the module. + promises.push(this.filepoolProvider.invalidateFilesByComponent(siteId, this.component, moduleId)); + promises.push(this.courseProvider.invalidateModule(moduleId, siteId)); + + // Also invalidate all the WS calls. + for (const method in this.handlerSchema.offlinefunctions) { + if (currentSite.wsAvailable(method)) { + // The method is a WS. + promises.push(currentSite.invalidateWsCacheForKey(this.sitePluginsProvider.getCallWSCacheKey(method, args))); + } else { + // It's a method to get content. + promises.push(this.sitePluginsProvider.invalidateContent(this.component, method, args)); + } + } + + return this.utils.allPromises(promises); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/core/siteplugins/classes/user-handler.ts b/src/core/siteplugins/classes/user-handler.ts new file mode 100644 index 000000000..0b003e56e --- /dev/null +++ b/src/core/siteplugins/classes/user-handler.ts @@ -0,0 +1,100 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NavController } from 'ionic-angular'; +import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; +import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to display a site plugin in the user profile. + */ +export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandler implements CoreUserProfileHandler { + /** + * The highest priority is displayed first. + * @type {number} + */ + priority: number; + + /** + * A type should be specified among these: + * - TYPE_COMMUNICATION: will be displayed under the user avatar. Should have icon. Spinner not used. + * - TYPE_NEW_PAGE: will be displayed as a list of items. Should have icon. Spinner not used. + * Default value if none is specified. + * - TYPE_ACTION: will be displayed as a button and should not redirect to any state. Spinner use is recommended. + * @type {string} + */ + type: string; + + constructor(name: string, protected title: string, protected plugin: any, protected handlerSchema: any, + protected bootstrapResult: any, protected sitePluginsProvider: CoreSitePluginsProvider) { + super(name); + + this.priority = handlerSchema.priority; + + // Only support TYPE_COMMUNICATION and TYPE_NEW_PAGE. + this.type = handlerSchema.type != CoreUserDelegate.TYPE_COMMUNICATION ? + CoreUserDelegate.TYPE_NEW_PAGE : CoreUserDelegate.TYPE_COMMUNICATION; + } + + /** + * Whether or not the handler is enabled for a user. + * @param {any} user User object. + * @param {number} courseId Course ID where to show. + * @param {any} [navOptions] Navigation options for the course. + * @param {any} [admOptions] Admin options for the course. + * @return {boolean|Promise} Whether or not the handler is enabled for a user. + */ + isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise { + // First check if it's enabled for the user. + const enabledForUser = this.sitePluginsProvider.isHandlerEnabledForUser(user.id, this.handlerSchema.restricttocurrentuser, + this.bootstrapResult.restrict); + if (!enabledForUser) { + return false; + } + + // Enabled for user, check if it's enabled for the course. + return this.sitePluginsProvider.isHandlerEnabledForCourse( + courseId, this.handlerSchema.restricttoenrolledcourses, this.bootstrapResult.restrict); + } + + /** + * Returns the data needed to render the handler. + * @param {any} user User object. + * @param {number} courseId Course ID where to show. + * @return {CoreUserProfileHandlerData} Data to be shown. + */ + getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData { + return { + title: this.title, + icon: this.handlerSchema.displaydata.icon, + class: this.handlerSchema.displaydata.class, + action: (event: Event, navCtrl: NavController, user: any, courseId?: number): void => { + event.preventDefault(); + event.stopPropagation(); + + navCtrl.push('CoreSitePluginsPluginPage', { + title: this.title, + component: this.plugin.component, + method: this.handlerSchema.method, + args: { + courseid: courseId, + userid: user.id + }, + bootstrapResult: this.bootstrapResult + }); + } + }; + } +} diff --git a/src/core/siteplugins/classes/user-profile-field-handler.ts b/src/core/siteplugins/classes/user-profile-field-handler.ts new file mode 100644 index 000000000..9fd8ed73b --- /dev/null +++ b/src/core/siteplugins/classes/user-profile-field-handler.ts @@ -0,0 +1,59 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Injector } from '@angular/core'; +import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@core/user/providers/user-profile-field-delegate'; +import { CoreSitePluginsBaseHandler } from './base-handler'; +import { CoreSitePluginsUserProfileFieldComponent } from '../components/user-profile-field/user-profile-field'; + +/** + * Handler to display a site plugin in the user profile. + */ +export class CoreSitePluginsUserProfileFieldHandler extends CoreSitePluginsBaseHandler implements CoreUserProfileFieldHandler { + + constructor(name: string) { + super(name); + } + + /** + * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreSitePluginsUserProfileFieldComponent; + } + + /** + * Get the data to send for the field based on the input data. + * @param {any} field User field to get the data for. + * @param {boolean} signup True if user is in signup page. + * @param {string} [registerAuth] Register auth method. E.g. 'email'. + * @param {any} formValues Form Values. + * @return {Promise|CoreUserProfileFieldHandlerData} Data to send for the field. + */ + getData(field: any, signup: boolean, registerAuth: string, formValues: any): + Promise | CoreUserProfileFieldHandlerData { + // No getData function implemented, use a default behaviour. + const name = 'profile_field_' + field.shortname; + + return { + type: field.type || field.datatype, + name: name, + value: formValues[name] + }; + } +} diff --git a/src/core/siteplugins/components/components.module.ts b/src/core/siteplugins/components/components.module.ts new file mode 100644 index 000000000..53a3d912c --- /dev/null +++ b/src/core/siteplugins/components/components.module.ts @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreComponentsModule } from '@components/components.module'; +import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module'; +import { CoreSitePluginsPluginContentComponent } from './plugin-content/plugin-content'; +import { CoreSitePluginsModuleIndexComponent } from './module-index/module-index'; +import { CoreSitePluginsCourseOptionComponent } from './course-option/course-option'; +import { CoreSitePluginsCourseFormatComponent } from './course-format/course-format'; +import { CoreSitePluginsUserProfileFieldComponent } from './user-profile-field/user-profile-field'; + +@NgModule({ + declarations: [ + CoreSitePluginsPluginContentComponent, + CoreSitePluginsModuleIndexComponent, + CoreSitePluginsCourseOptionComponent, + CoreSitePluginsCourseFormatComponent, + CoreSitePluginsUserProfileFieldComponent + ], + imports: [ + CommonModule, + IonicModule, + CoreComponentsModule, + CoreCompileHtmlComponentModule, + TranslateModule.forChild() + ], + providers: [ + ], + exports: [ + CoreSitePluginsPluginContentComponent, + CoreSitePluginsModuleIndexComponent, + CoreSitePluginsCourseOptionComponent, + CoreSitePluginsCourseFormatComponent, + CoreSitePluginsUserProfileFieldComponent + ], + entryComponents: [ + CoreSitePluginsModuleIndexComponent, + CoreSitePluginsCourseOptionComponent, + CoreSitePluginsCourseFormatComponent, + CoreSitePluginsUserProfileFieldComponent + ] +}) +export class CoreSitePluginsComponentsModule {} diff --git a/src/core/siteplugins/components/course-format/course-format.html b/src/core/siteplugins/components/course-format/course-format.html new file mode 100644 index 000000000..b4dd6868b --- /dev/null +++ b/src/core/siteplugins/components/course-format/course-format.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/core/siteplugins/components/course-format/course-format.ts b/src/core/siteplugins/components/course-format/course-format.ts new file mode 100644 index 000000000..72a5972f4 --- /dev/null +++ b/src/core/siteplugins/components/course-format/course-format.ts @@ -0,0 +1,68 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnInit, Input, ViewChild } from '@angular/core'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; + +/** + * Component that displays the index of a course format site plugin. + */ +@Component({ + selector: 'core-site-plugins-course-format', + templateUrl: 'course-format.html', +}) +export class CoreSitePluginsCourseFormatComponent implements OnInit { + @Input() course: any; // The course to render. + @Input() sections: any[]; // List of course sections. + @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. + + @ViewChild(CoreSitePluginsPluginContentComponent) content: CoreSitePluginsPluginContentComponent; + + component: string; + method: string; + args: any; + bootstrapResult: any; + + constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + if (this.course && this.course.format) { + const handler = this.sitePluginsProvider.getSitePluginHandler(this.course.format); + if (handler) { + this.component = handler.plugin.component; + this.method = handler.handlerSchema.method; + this.args = { + courseid: this.course.id, + downloadenabled: this.downloadEnabled + }; + this.bootstrapResult = handler.bootstrapResult; + } + } + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @return {Promise} Promise resolved when done. + */ + doRefresh(refresher?: any, done?: () => void): Promise { + return Promise.resolve(this.content.refreshData()); + } +} diff --git a/src/core/siteplugins/components/course-option/course-option.html b/src/core/siteplugins/components/course-option/course-option.html new file mode 100644 index 000000000..fcda25ccd --- /dev/null +++ b/src/core/siteplugins/components/course-option/course-option.html @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/core/siteplugins/components/course-option/course-option.ts b/src/core/siteplugins/components/course-option/course-option.ts new file mode 100644 index 000000000..b14e67b76 --- /dev/null +++ b/src/core/siteplugins/components/course-option/course-option.ts @@ -0,0 +1,66 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnInit, Input, ViewChild } from '@angular/core'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; + +/** + * Component that displays the index of a course option site plugin. + */ +@Component({ + selector: 'core-site-plugins-course-option', + templateUrl: 'course-option.html', +}) +export class CoreSitePluginsCourseOptionComponent implements OnInit { + @Input() courseId: number; + @Input() handlerUniqueName: string; + + @ViewChild(CoreSitePluginsPluginContentComponent) content: CoreSitePluginsPluginContentComponent; + + component: string; + method: string; + args: any; + bootstrapResult: any; + + constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + if (this.handlerUniqueName) { + const handler = this.sitePluginsProvider.getSitePluginHandler(this.handlerUniqueName); + if (handler) { + this.component = handler.plugin.component; + this.method = handler.handlerSchema.method; + this.args = { + courseid: this.courseId, + }; + this.bootstrapResult = handler.bootstrapResult; + } + } + } + + /** + * Refresh the data. + * + * @param {any} refresher Refresher. + */ + refreshData(refresher: any): void { + this.content.refreshData().finally(() => { + refresher.complete(); + }); + } +} diff --git a/src/core/siteplugins/components/module-index/module-index.html b/src/core/siteplugins/components/module-index/module-index.html new file mode 100644 index 000000000..bee280179 --- /dev/null +++ b/src/core/siteplugins/components/module-index/module-index.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/core/siteplugins/components/module-index/module-index.ts b/src/core/siteplugins/components/module-index/module-index.ts new file mode 100644 index 000000000..c3cfb5e01 --- /dev/null +++ b/src/core/siteplugins/components/module-index/module-index.ts @@ -0,0 +1,151 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnInit, OnDestroy, Input, ViewChild } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreCourseModuleMainComponent } from '@core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; + +/** + * Component that displays the index of a module site plugin. + */ +@Component({ + selector: 'core-site-plugins-module-index', + templateUrl: 'module-index.html', +}) +export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, CoreCourseModuleMainComponent { + @Input() module: any; // The module. + @Input() courseId: number; // Course ID the module belongs to. + + @ViewChild(CoreSitePluginsPluginContentComponent) content: CoreSitePluginsPluginContentComponent; + + component: string; + method: string; + args: any; + bootstrapResult: any; + + // Data for context menu. + externalUrl: string; + description: string; + refreshIcon: string; + prefetchStatusIcon: string; + prefetchText: string; + size: string; + + protected isDestroyed = false; + protected statusObserver; + + constructor(protected sitePluginsProvider: CoreSitePluginsProvider, protected courseHelper: CoreCourseHelperProvider, + protected prefetchDelegate: CoreCourseModulePrefetchDelegate, protected textUtils: CoreTextUtilsProvider, + protected translate: TranslateService) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.refreshIcon = 'spinner'; + + if (this.module) { + const handler = this.sitePluginsProvider.getSitePluginHandler(this.module.modname); + if (handler) { + this.component = handler.plugin.component; + this.method = handler.handlerSchema.method; + this.args = { + courseid: this.courseId, + cmid: this.module.id + }; + this.bootstrapResult = handler.bootstrapResult; + } + + // Get the data for the context menu. + this.description = this.module.description; + this.externalUrl = this.module.url; + } + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @return {Promise} Promise resolved when done. + */ + doRefresh(refresher?: any, done?: () => void): Promise { + if (this.content) { + this.refreshIcon = 'spinner'; + + return Promise.resolve(this.content.refreshData()).finally(() => { + refresher && refresher.complete(); + done && done(); + }); + } else { + refresher && refresher.complete(); + done && done(); + + return Promise.resolve(); + } + } + + /** + * Function called when the data of the site plugin content is loaded. + */ + contentLoaded(refresh: boolean): void { + this.refreshIcon = 'refresh'; + + // Check if there is a prefetch handler for this type of module. + if (this.prefetchDelegate.getPrefetchHandlerFor(this.module)) { + this.courseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component); + } + } + + /** + * Function called when starting to load the data of the site plugin content. + */ + contentLoading(refresh: boolean): void { + this.refreshIcon = 'spinner'; + } + + /** + * Expand the description. + */ + expandDescription(): void { + this.textUtils.expandText(this.translate.instant('core.description'), this.description, this.component, this.module.id); + } + + /** + * Prefetch the module. + */ + prefetch(): void { + this.courseHelper.contextMenuPrefetch(this, this.module, this.courseId); + } + + /** + * Confirm and remove downloaded files. + */ + removeFiles(): void { + this.courseHelper.confirmAndRemoveFiles(this.module, this.courseId); + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + this.statusObserver && this.statusObserver.off(); + } +} diff --git a/src/core/siteplugins/components/plugin-content/plugin-content.html b/src/core/siteplugins/components/plugin-content/plugin-content.html new file mode 100644 index 000000000..a22885173 --- /dev/null +++ b/src/core/siteplugins/components/plugin-content/plugin-content.html @@ -0,0 +1,3 @@ + + + diff --git a/src/core/siteplugins/components/plugin-content/plugin-content.ts b/src/core/siteplugins/components/plugin-content/plugin-content.ts new file mode 100644 index 000000000..0e31421ea --- /dev/null +++ b/src/core/siteplugins/components/plugin-content/plugin-content.ts @@ -0,0 +1,110 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { Subject } from 'rxjs'; + +/** + * Component to render a site plugin content. + */ +@Component({ + selector: 'core-site-plugins-plugin-content', + templateUrl: 'plugin-content.html', +}) +export class CoreSitePluginsPluginContentComponent implements OnInit { + @Input() component: string; + @Input() method: string; + @Input() args: any; + @Input() bootstrapResult: any; // Result of the bootstrap WS call of the handler. + @Output() onContentLoaded?: EventEmitter; // Emits an event when the content is loaded. + @Output() onLoadingContent?: EventEmitter; // Emits an event when starts to load the content. + + content: string; // Content. + javascript: string; // Javascript to execute. + otherData: any; // Other data of the content. + dataLoaded: boolean; + invalidateObservable: Subject; // An observable to notify observers when to invalidate data. + jsData: any; // Data to pass to the component. + + constructor(protected domUtils: CoreDomUtilsProvider, protected sitePluginsProvider: CoreSitePluginsProvider) { + this.onContentLoaded = new EventEmitter(); + this.onLoadingContent = new EventEmitter(); + this.invalidateObservable = new Subject(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchContent(); + } + + /** + * Fetches the content to render. + * + * @param {boolean} [refresh] Whether the user is refreshing. + * @return {Promise} Promise resolved when done. + */ + fetchContent(refresh?: boolean): Promise { + this.onLoadingContent.emit(refresh); + + return this.sitePluginsProvider.getContent(this.component, this.method, this.args).then((result) => { + this.content = result.templates.length ? result.templates[0].html : ''; // Load first template. + this.javascript = result.javascript; + this.otherData = result.otherdata; + this.jsData = this.sitePluginsProvider.createDataForJS(this.bootstrapResult, result); + + this.onContentLoaded.emit(refresh); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); + }).finally(() => { + this.dataLoaded = true; + }); + } + + /** + * Refresh the data. + * + * @param {boolean} [showSpinner] Whether to show spinner while refreshing. + */ + refreshData(showSpinner?: boolean): Promise { + if (showSpinner) { + this.dataLoaded = false; + } + + this.invalidateObservable.next(); // Notify observers. + + return this.sitePluginsProvider.invalidateContent(this.component, this.method, this.args).finally(() => { + return this.fetchContent(true); + }); + } + + /** + * Update the content, usually with a different method or params. + * + * @param {string} component New component. + * @param {string} method New method. + * @param {any} args New params. + */ + updateContent(component: string, method: string, args: any): void { + this.component = component; + this.method = method; + this.args = args; + this.dataLoaded = false; + + this.fetchContent(); + } +} diff --git a/src/core/siteplugins/components/user-profile-field/user-profile-field.html b/src/core/siteplugins/components/user-profile-field/user-profile-field.html new file mode 100644 index 000000000..fec5e4726 --- /dev/null +++ b/src/core/siteplugins/components/user-profile-field/user-profile-field.html @@ -0,0 +1 @@ + diff --git a/src/core/siteplugins/components/user-profile-field/user-profile-field.ts b/src/core/siteplugins/components/user-profile-field/user-profile-field.ts new file mode 100644 index 000000000..84030ba62 --- /dev/null +++ b/src/core/siteplugins/components/user-profile-field/user-profile-field.ts @@ -0,0 +1,87 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnInit, Input, ViewChild, OnDestroy } from '@angular/core'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreCompileHtmlComponent } from '@core/compile/components/compile-html/compile-html'; +import { Subscription } from 'rxjs'; + +/** + * Component that displays a user profile field created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-user-profile-field', + templateUrl: 'user-profile-field.html', +}) +export class CoreSitePluginsUserProfileFieldComponent implements OnInit, OnDestroy { + @Input() field: any; // The profile field to be rendered. + @Input() signup = false; // True if editing the field in signup. Defaults to false. + @Input() edit = false; // True if editing the field. Defaults to false. + @Input() form?: any; // Form where to add the form control. Required if edit=true or signup=true. + @Input() registerAuth?: string; // Register auth method. E.g. 'email'. + + @ViewChild(CoreCompileHtmlComponent) compileComponent: CoreCompileHtmlComponent; + + content = ''; // Content. + jsData; + protected componentObserver: Subscription; + + constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + + // Pass the input data to the component. + this.jsData = { + field: this.field, + signup: this.signup, + edit: this.edit, + form: this.form, + registerAuth: this.registerAuth + }; + + if (this.field) { + // Retrieve the handler data. + const handler = this.sitePluginsProvider.getSitePluginHandler(this.field.type || this.field.datatype), + handlerSchema = handler && handler.handlerSchema; + + if (handlerSchema) { + // Load first template. + if (handlerSchema.methodTemplates && handlerSchema.methodTemplates.length) { + this.content = handler.handlerSchema.methodTemplates[0].html; + } + + // Wait for the instance to be created. + if (this.compileComponent && this.compileComponent.componentObservable && + handlerSchema.methodJSResult && handlerSchema.methodJSResult.componentInit) { + this.componentObserver = this.compileComponent.componentObservable.subscribe((instance) => { + if (instance) { + // Instance created, call component init. + handlerSchema.methodJSResult.componentInit.apply(instance); + } + }); + } + } + } + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.componentObserver && this.componentObserver.unsubscribe(); + } +} diff --git a/src/core/siteplugins/directives/call-ws-new-content.ts b/src/core/siteplugins/directives/call-ws-new-content.ts new file mode 100644 index 000000000..7f1346ef5 --- /dev/null +++ b/src/core/siteplugins/directives/call-ws-new-content.ts @@ -0,0 +1,99 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Directive, Input, ElementRef, Optional } from '@angular/core'; +import { NavController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreSitePluginsCallWSOnClickBaseDirective } from '../classes/call-ws-click-directive'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; + +/** + * Directive to call a WS when the element is clicked and load a new content passing the WS result as args. This new content + * can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content). + * + * If you don't need to load some new content when done, @see CoreSitePluginsCallWSDirective. + * + * @see CoreSitePluginsCallWSOnClickBaseDirective. + * + * Example usages: + * + * A button to get some data from the server without using cache, showing default confirm and displaying a new page: + * + * + * + * A button to get some data from the server using cache, without confirm, displaying new content in same page and using + * userid from otherdata: + * + * + */ +@Directive({ + selector: '[core-site-plugins-call-ws-new-content]' +}) +export class CoreSitePluginsCallWSNewContentDirective extends CoreSitePluginsCallWSOnClickBaseDirective { + @Input() component: string; // The component of the new content. + @Input() method: string; // The method to get the new content. + @Input() args: any; // The params to get the new content. + @Input() title: string; // The title to display with the new content. Only if samePage=false. + @Input() samePage: boolean | string; // Whether to display the content in same page or open a new one. Defaults to new page. + @Input() useOtherData: any[]; // Whether to include other data in the args. @see CoreSitePluginsProvider.loadOtherDataInArgs. + + constructor(element: ElementRef, translate: TranslateService, domUtils: CoreDomUtilsProvider, + sitePluginsProvider: CoreSitePluginsProvider, @Optional() parentContent: CoreSitePluginsPluginContentComponent, + protected utils: CoreUtilsProvider, @Optional() protected navCtrl: NavController) { + super(element, translate, domUtils, sitePluginsProvider, parentContent); + } + + /** + * Function called when the WS call is successful. + * + * @param {any} result Result of the WS call. + */ + protected wsCallSuccess(result: any): void { + let args = this.args || {}; + + if (this.parentContent) { + args = this.sitePluginsProvider.loadOtherDataInArgs(this.args, this.parentContent.otherData, this.useOtherData); + } + + // Add the properties from the WS call result to the args. + args = Object.assign(args, result); + + if (this.utils.isTrueOrOne(this.samePage)) { + // Update the parent content (if it exists). + if (this.parentContent) { + this.parentContent.updateContent(this.component, this.method, args); + } + } else { + this.navCtrl.push('CoreSitePluginsPluginPage', { + title: this.title, + component: this.component, + method: this.method, + args: args, + bootstrapResult: this.parentContent && this.parentContent.bootstrapResult + }); + } + } +} diff --git a/src/core/siteplugins/directives/call-ws-on-load.ts b/src/core/siteplugins/directives/call-ws-on-load.ts new file mode 100644 index 000000000..14290e22a --- /dev/null +++ b/src/core/siteplugins/directives/call-ws-on-load.ts @@ -0,0 +1,57 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreSitePluginsCallWSBaseDirective } from '../classes/call-ws-directive'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; + +/** + * Directive to call a WS as soon as its loaded. + * This directive is meant for actions to do in the background, like calling logging WebServices. + * + * If you want to call a WS when the user clicks on a certain element, @see CoreSitePluginsCallWSDirective. + * + * @see CoreSitePluginsCallWSBaseDirective. + * + * Example usage: + * + * + */ +@Directive({ + selector: '[core-site-plugins-call-ws-on-load]' +}) +export class CoreSitePluginsCallWSOnLoadDirective extends CoreSitePluginsCallWSBaseDirective implements OnInit { + + constructor(element: ElementRef, translate: TranslateService, domUtils: CoreDomUtilsProvider, + sitePluginsProvider: CoreSitePluginsProvider, @Optional() parentContent: CoreSitePluginsPluginContentComponent) { + super(element, translate, domUtils, sitePluginsProvider, parentContent); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + // Call the WS immediately. + this.callWS().catch(() => { + // Ignore errors. + }); + } +} diff --git a/src/core/siteplugins/directives/call-ws.ts b/src/core/siteplugins/directives/call-ws.ts new file mode 100644 index 000000000..ee6034380 --- /dev/null +++ b/src/core/siteplugins/directives/call-ws.ts @@ -0,0 +1,80 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Directive, Input, ElementRef, Optional } from '@angular/core'; +import { NavController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreSitePluginsCallWSOnClickBaseDirective } from '../classes/call-ws-click-directive'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; + +/** + * Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the input data: + * display a message, go back or refresh current view. + * + * If you want to load a new content when the WS call is done, @see CoreSitePluginsCallWSNewContentDirective. + * + * @see CoreSitePluginsCallWSOnClickBaseDirective. + * + * Example usages: + * + * A button to send some data to the server without using cache, displaying default messages and refreshing on success: + * + * + * + * A button to send some data to the server using cache, without confirm, going back on success and using userid from otherdata: + * + * + */ +@Directive({ + selector: '[core-site-plugins-call-ws]' +}) +export class CoreSitePluginsCallWSDirective extends CoreSitePluginsCallWSOnClickBaseDirective { + @Input() successMessage: string; // Message to show on success. If not supplied, no message. If empty, default message. + @Input() goBackOnSuccess: boolean | string; // Whether to go back if the WS call is successful. + @Input() refreshOnSuccess: boolean | string; // Whether to refresh the current view if the WS call is successful. + + constructor(element: ElementRef, translate: TranslateService, domUtils: CoreDomUtilsProvider, + sitePluginsProvider: CoreSitePluginsProvider, @Optional() parentContent: CoreSitePluginsPluginContentComponent, + protected utils: CoreUtilsProvider, protected navCtrl: NavController) { + super(element, translate, domUtils, sitePluginsProvider, parentContent); + } + + /** + * Function called when the WS call is successful. + * + * @param {any} result Result of the WS call. + */ + protected wsCallSuccess(result: any): void { + if (typeof this.successMessage != 'undefined') { + // Display the success message. + this.domUtils.showToast(this.successMessage || this.translate.instant('core.success')); + } + + if (this.utils.isTrueOrOne(this.goBackOnSuccess)) { + this.navCtrl.pop(); + } else if (this.utils.isTrueOrOne(this.refreshOnSuccess) && this.parentContent) { + this.parentContent.refreshData(true); + } + } +} diff --git a/src/core/siteplugins/directives/directives.module.ts b/src/core/siteplugins/directives/directives.module.ts new file mode 100644 index 000000000..4197acace --- /dev/null +++ b/src/core/siteplugins/directives/directives.module.ts @@ -0,0 +1,36 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSitePluginsCallWSDirective } from './call-ws'; +import { CoreSitePluginsCallWSNewContentDirective } from './call-ws-new-content'; +import { CoreSitePluginsCallWSOnLoadDirective } from './call-ws-on-load'; +import { CoreSitePluginsNewContentDirective } from './new-content'; + +@NgModule({ + declarations: [ + CoreSitePluginsCallWSDirective, + CoreSitePluginsCallWSNewContentDirective, + CoreSitePluginsCallWSOnLoadDirective, + CoreSitePluginsNewContentDirective + ], + imports: [], + exports: [ + CoreSitePluginsCallWSDirective, + CoreSitePluginsCallWSNewContentDirective, + CoreSitePluginsCallWSOnLoadDirective, + CoreSitePluginsNewContentDirective + ] +}) +export class CoreSitePluginsDirectivesModule {} diff --git a/src/core/siteplugins/directives/new-content.ts b/src/core/siteplugins/directives/new-content.ts new file mode 100644 index 000000000..6503ed5a2 --- /dev/null +++ b/src/core/siteplugins/directives/new-content.ts @@ -0,0 +1,97 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; +import { NavController } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; + +/** + * Directive to display a new site plugin content when clicked. This new content can be displayed in a new page or in the + * current page (only if the current page is already displaying a site plugin content). + * + * Example usages: + * + * A button to go to a new content page: + * + * + * + * A button to load new content in current page using a param from otherdata: + * + * + */ +@Directive({ + selector: '[core-site-plugins-new-content]' +}) +export class CoreSitePluginsNewContentDirective implements OnInit { + @Input() component: string; // The component of the new content. + @Input() method: string; // The method to get the new content. + @Input() args: any; // The params to get the new content. + @Input() title: string; // The title to display with the new content. Only if samePage=false. + @Input() samePage: boolean | string; // Whether to display the content in same page or open a new one. Defaults to new page. + @Input() useOtherData: any[]; // Whether to include other data in the args. @see CoreSitePluginsProvider.loadOtherDataInArgs. + @Input() form: string; // ID or name to identify a form. The form will be obtained from document.forms. + // If supplied and form is found, the form data will be retrieved and sent to the new content. + + protected element: HTMLElement; + + constructor(element: ElementRef, protected utils: CoreUtilsProvider, @Optional() protected navCtrl: NavController, + @Optional() protected parentContent: CoreSitePluginsPluginContentComponent, protected domUtils: CoreDomUtilsProvider, + protected sitePluginsProvider: CoreSitePluginsProvider) { + this.element = element.nativeElement || element; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.element.addEventListener('click', (ev: Event): void => { + ev.preventDefault(); + ev.stopPropagation(); + + let args = this.args || {}; + + if (this.parentContent) { + args = this.sitePluginsProvider.loadOtherDataInArgs(this.args, this.parentContent.otherData, this.useOtherData); + } + + if (this.form && document.forms[this.form]) { + args = Object.assign(args, this.domUtils.getDataFromForm(document.forms[this.form])); + } + + if (this.utils.isTrueOrOne(this.samePage)) { + // Update the parent content (if it exists). + if (this.parentContent) { + this.parentContent.updateContent(this.component, this.method, args); + } + } else { + this.navCtrl.push('CoreSitePluginsPluginPage', { + title: this.title, + component: this.component, + method: this.method, + args: args, + bootstrapResult: this.parentContent && this.parentContent.bootstrapResult + }); + } + }); + } +} diff --git a/src/core/siteplugins/pages/module-index/module-index.html b/src/core/siteplugins/pages/module-index/module-index.html new file mode 100644 index 000000000..cf7a5dfad --- /dev/null +++ b/src/core/siteplugins/pages/module-index/module-index.html @@ -0,0 +1,15 @@ + + + {{ title }} + + + + + + + + + + + + diff --git a/src/core/siteplugins/pages/module-index/module-index.module.ts b/src/core/siteplugins/pages/module-index/module-index.module.ts new file mode 100644 index 000000000..cd9e5a003 --- /dev/null +++ b/src/core/siteplugins/pages/module-index/module-index.module.ts @@ -0,0 +1,34 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreSitePluginsModuleIndexPage } from './module-index'; +import { CoreSitePluginsComponentsModule } from '../../components/components.module'; + +/** + * Module to lazy load the page. + */ +@NgModule({ + declarations: [ + CoreSitePluginsModuleIndexPage + ], + imports: [ + CoreSitePluginsComponentsModule, + IonicPageModule.forChild(CoreSitePluginsModuleIndexPage), + TranslateModule.forChild() + ] +}) +export class CoreSitePluginsModuleIndexPageModule {} diff --git a/src/core/siteplugins/pages/module-index/module-index.ts b/src/core/siteplugins/pages/module-index/module-index.ts new file mode 100644 index 000000000..de4050eb3 --- /dev/null +++ b/src/core/siteplugins/pages/module-index/module-index.ts @@ -0,0 +1,51 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, ViewChild } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreSitePluginsModuleIndexComponent } from '../../components/module-index/module-index'; + +/** + * Page to render the index page of a module site plugin. + */ +@IonicPage({ segment: 'core-site-plugins-module-index-page' }) +@Component({ + selector: 'page-core-site-plugins-module-index', + templateUrl: 'module-index.html', +}) +export class CoreSitePluginsModuleIndexPage { + @ViewChild(CoreSitePluginsModuleIndexComponent) content: CoreSitePluginsModuleIndexComponent; + + title: string; // Page title. + + module: any; + courseId: number; + + constructor(params: NavParams) { + this.title = params.get('title'); + this.module = params.get('module'); + this.courseId = params.get('courseId'); + } + + /** + * Refresh the data. + * + * @param {any} refresher Refresher. + */ + refreshData(refresher: any): void { + this.content.doRefresh().finally(() => { + refresher.complete(); + }); + } +} diff --git a/src/core/siteplugins/pages/plugin-page/plugin-page.html b/src/core/siteplugins/pages/plugin-page/plugin-page.html new file mode 100644 index 000000000..a7a553fbe --- /dev/null +++ b/src/core/siteplugins/pages/plugin-page/plugin-page.html @@ -0,0 +1,15 @@ + + + {{ title }} + + + + + + + + + + + + diff --git a/src/core/siteplugins/pages/plugin-page/plugin-page.module.ts b/src/core/siteplugins/pages/plugin-page/plugin-page.module.ts new file mode 100644 index 000000000..396df5702 --- /dev/null +++ b/src/core/siteplugins/pages/plugin-page/plugin-page.module.ts @@ -0,0 +1,34 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreSitePluginsPluginPage } from './plugin-page'; +import { CoreSitePluginsComponentsModule } from '../../components/components.module'; + +/** + * Module to lazy load the page. + */ +@NgModule({ + declarations: [ + CoreSitePluginsPluginPage + ], + imports: [ + CoreSitePluginsComponentsModule, + IonicPageModule.forChild(CoreSitePluginsPluginPage), + TranslateModule.forChild() + ] +}) +export class CoreSitePluginsPluginPageModule {} diff --git a/src/core/siteplugins/pages/plugin-page/plugin-page.ts b/src/core/siteplugins/pages/plugin-page/plugin-page.ts new file mode 100644 index 000000000..38d516a47 --- /dev/null +++ b/src/core/siteplugins/pages/plugin-page/plugin-page.ts @@ -0,0 +1,55 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, ViewChild } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreSitePluginsPluginContentComponent } from '../../components/plugin-content/plugin-content'; + +/** + * Page to render a site plugin page. + */ +@IonicPage({ segment: 'core-site-plugins-plugin-page' }) +@Component({ + selector: 'page-core-site-plugins-plugin', + templateUrl: 'plugin-page.html', +}) +export class CoreSitePluginsPluginPage { + @ViewChild(CoreSitePluginsPluginContentComponent) content: CoreSitePluginsPluginContentComponent; + + title: string; // Page title. + + component: string; + method: string; + args: any; + bootstrapResult: any; + + constructor(params: NavParams) { + this.title = params.get('title'); + this.component = params.get('component'); + this.method = params.get('method'); + this.args = params.get('args'); + this.bootstrapResult = params.get('bootstrapResult'); + } + + /** + * Refresh the data. + * + * @param {any} refresher Refresher. + */ + refreshData(refresher: any): void { + this.content.refreshData().finally(() => { + refresher.complete(); + }); + } +} diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts new file mode 100644 index 000000000..88f1c0013 --- /dev/null +++ b/src/core/siteplugins/providers/helper.ts @@ -0,0 +1,529 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLangProvider } from '@providers/lang'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSite } from '@classes/site'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSitePluginsProvider } from './siteplugins'; +import { CoreCompileProvider } from '@core/compile/providers/compile'; + +// Delegates +import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; +import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate'; +import { CoreUserDelegate } from '@core/user/providers/user-delegate'; +import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; + +// Handler classes. +import { CoreSitePluginsCourseFormatHandler } from '../classes/course-format-handler'; +import { CoreSitePluginsCourseOptionHandler } from '../classes/course-option-handler'; +import { CoreSitePluginsModuleHandler } from '../classes/module-handler'; +import { CoreSitePluginsModulePrefetchHandler } from '../classes/module-prefetch-handler'; +import { CoreSitePluginsMainMenuHandler } from '../classes/main-menu-handler'; +import { CoreSitePluginsUserProfileHandler } from '../classes/user-handler'; +import { CoreSitePluginsUserProfileFieldHandler } from '../classes/user-profile-field-handler'; + +/** + * Helper service to provide functionalities regarding site plugins. It basically has the features to load and register site + * plugin. + * + * This code is split from CoreSitePluginsProvider to prevent circular dependencies. + * + * @todo: Support ViewChild and similar in site plugins. Possible solution: make components and directives inject the instance + * inside the host DOM element? + */ +@Injectable() +export class CoreSitePluginsHelperProvider { + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private injector: Injector, + private mainMenuDelegate: CoreMainMenuDelegate, private moduleDelegate: CoreCourseModuleDelegate, + private userDelegate: CoreUserDelegate, private langProvider: CoreLangProvider, + private sitePluginsProvider: CoreSitePluginsProvider, private prefetchDelegate: CoreCourseModulePrefetchDelegate, + private compileProvider: CoreCompileProvider, private utils: CoreUtilsProvider, + private courseOptionsDelegate: CoreCourseOptionsDelegate, eventsProvider: CoreEventsProvider, + private courseFormatDelegate: CoreCourseFormatDelegate, private profileFieldDelegate: CoreUserProfileFieldDelegate, + private textUtils: CoreTextUtilsProvider) { + this.logger = logger.getInstance('CoreSitePluginsHelperProvider'); + + // Fetch the plugins on login. + eventsProvider.on(CoreEventsProvider.LOGIN, () => { + const siteId = this.sitesProvider.getCurrentSiteId(); + this.fetchSitePlugins(siteId).then((plugins) => { + // Plugins fetched, check that site hasn't changed. + if (siteId == this.sitesProvider.getCurrentSiteId() && plugins.length) { + // Site is still the same. Load the plugins and trigger the event. + this.loadSitePlugins(plugins).then(() => { + eventsProvider.trigger(CoreEventsProvider.SITE_PLUGINS_LOADED, {}, siteId); + }); + + } + }); + }); + + // Unload plugins on logout if any. + eventsProvider.on(CoreEventsProvider.LOGOUT, () => { + if (this.sitePluginsProvider.hasSitePluginsLoaded) { + // Temporary fix. Reload the page to unload all plugins. + window.location.reload(); + } + }); + } + + /** + * Bootstrap a handler if it has some bootstrap method. + * + * @param {any} plugin Data of the plugin. + * @param {any} handlerSchema Data about the handler. + * @return {Promise} Promise resolved when done. It returns the results of the getContent call and the data returned by + * the bootstrap JS (if any). + */ + protected bootstrapHandler(plugin: any, handlerSchema: any): Promise { + if (!handlerSchema.bootstrap) { + return Promise.resolve({}); + } + + return this.executeMethodAndJS(plugin, handlerSchema.bootstrap); + } + + /** + * Execute a get_content method and run its javascript (if any). + * + * @param {any} plugin Data of the plugin. + * @param {string} method The method to call. + * @return {Promise} Promise resolved when done. It returns the results of the getContent call and the data returned by + * the JS (if any). + */ + protected executeMethodAndJS(plugin: any, method: string): Promise { + const siteId = this.sitesProvider.getCurrentSiteId(), + preSets = {getFromCache: false}; // Try to ignore cache. + + return this.sitePluginsProvider.getContent(plugin.component, method, {}, preSets).then((result) => { + if (!result.javascript || this.sitesProvider.getCurrentSiteId() != siteId) { + // No javascript or site has changed, stop. + return result; + } + + // Create a "fake" instance to hold all the libraries. + const instance = {}; + this.compileProvider.injectLibraries(instance); + + // Add some data of the WS call result. + const jsData = this.sitePluginsProvider.createDataForJS(result); + for (const name in jsData) { + instance[name] = jsData[name]; + } + + // Now execute the javascript using this instance. + result.jsResult = this.compileProvider.executeJavascript(instance, result.javascript); + + return result; + }); + } + + /** + * Fetch site plugins. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. Returns the list of plugins to load. + */ + fetchSitePlugins(siteId?: string): Promise { + const plugins = []; + + return this.sitesProvider.getSite(siteId).then((site) => { + if (!this.sitePluginsProvider.isGetContentAvailable(site)) { + // Cannot load site plugins, so there's no point to fetch them. + return plugins; + } + + // Get the list of plugins. Try not to use cache. + return site.read('tool_mobile_get_plugins_supporting_mobile', {}, { getFromCache: false }).then((data) => { + data.plugins.forEach((plugin: any) => { + // Check if it's a site plugin and it's enabled. + if (this.isSitePluginEnabled(plugin, site)) { + plugins.push(plugin); + } + }); + + return plugins; + }); + }); + } + + /** + * Given an addon name, return the prefix to add to its string keys. + * + * @param {string} addon Name of the addon (plugin.addon). + * @return {string} Prefix. + */ + protected getPrefixForStrings(addon: string): string { + if (addon) { + return 'plugin.' + addon + '.'; + } + + return ''; + } + + /** + * Given an addon name and the key of a string, return the full string key (prefixed). + * + * @param {string} addon Name of the addon (plugin.addon). + * @param {string} key The key of the string. + * @return {string} Full string key. + */ + protected getPrefixedString(addon: string, key: string): string { + return this.getPrefixForStrings(addon) + key; + } + + /** + * Check if a certain plugin is a site plugin and it's enabled in a certain site. + * + * @param {any} plugin Data of the plugin. + * @param {CoreSite} site Site affected. + * @return {boolean} Whether it's a site plugin and it's enabled. + */ + isSitePluginEnabled(plugin: any, site: CoreSite): boolean { + if (!site.isFeatureDisabled('sitePlugin_' + plugin.component + '_' + plugin.addon) && plugin.handlers) { + // Site plugin not disabled. Check if it has handlers. + if (!plugin.parsedHandlers) { + plugin.parsedHandlers = this.textUtils.parseJSON(plugin.handlers, null, + this.logger.error.bind(this.logger, 'Error parsing site plugin handlers')); + } + + return !!(plugin.parsedHandlers && Object.keys(plugin.parsedHandlers).length); + } + + return false; + } + + /** + * Load the lang strings for a plugin. + * + * @param {any} plugin Data of the plugin. + */ + loadLangStrings(plugin: any): void { + if (!plugin.parsedLang) { + return; + } + + for (const lang in plugin.parsedLang) { + const prefix = this.getPrefixForStrings(plugin.addon); + + this.langProvider.addSitePluginsStrings(lang, plugin.parsedLang[lang], prefix); + } + } + + /** + * Load a site plugin. + * + * @param {any} plugin Data of the plugin. + * @return {Promise} Promise resolved when loaded. + */ + loadSitePlugin(plugin: any): Promise { + const promises = []; + + this.logger.debug('Load site plugin:', plugin); + + if (!plugin.parsedHandlers) { + plugin.parsedHandlers = this.textUtils.parseJSON(plugin.handlers, null, + this.logger.error.bind(this.logger, 'Error parsing site plugin handlers')); + } + if (!plugin.parsedLang && plugin.lang) { + plugin.parsedLang = this.textUtils.parseJSON(plugin.lang, null, + this.logger.error.bind(this.logger, 'Error parsing site plugin lang')); + } + + this.sitePluginsProvider.hasSitePluginsLoaded = true; + + // Register lang strings. + this.loadLangStrings(plugin); + + // Register all the handlers. + for (const name in plugin.parsedHandlers) { + promises.push(this.registerHandler(plugin, name, plugin.parsedHandlers[name])); + } + + return this.utils.allPromises(promises); + } + + /** + * Load site plugins. + * + * @param {any[]} plugins The plugins to load. + * @return {Promise} Promise resolved when loaded. + */ + loadSitePlugins(plugins: any[]): Promise { + const promises = []; + + plugins.forEach((plugin) => { + promises.push(this.loadSitePlugin(plugin)); + }); + + return this.utils.allPromises(promises); + } + + /** + * Register a site plugin handler in the right delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @return {Promise} Promise resolved when done. + */ + registerHandler(plugin: any, handlerName: string, handlerSchema: any): Promise { + + // Wait for the bootstrap JS to be executed. + return this.bootstrapHandler(plugin, handlerSchema).then((result) => { + let promise; + + switch (handlerSchema.delegate) { + case 'CoreMainMenuDelegate': + promise = Promise.resolve(this.registerMainMenuHandler(plugin, handlerName, handlerSchema, result)); + break; + + case 'CoreCourseModuleDelegate': + promise = Promise.resolve(this.registerModuleHandler(plugin, handlerName, handlerSchema, result)); + break; + + case 'CoreUserDelegate': + promise = Promise.resolve(this.registerUserProfileHandler(plugin, handlerName, handlerSchema, result)); + break; + + case 'CoreCourseOptionsDelegate': + promise = Promise.resolve(this.registerCourseOptionHandler(plugin, handlerName, handlerSchema, result)); + break; + + case 'CoreCourseFormatDelegate': + promise = Promise.resolve(this.registerCourseFormatHandler(plugin, handlerName, handlerSchema, result)); + break; + + case 'CoreUserProfileFieldDelegate': + promise = Promise.resolve(this.registerUserProfileFieldHandler(plugin, handlerName, handlerSchema, result)); + break; + + default: + // Nothing to do. + promise = Promise.resolve(); + } + + return promise.then((uniqueName) => { + if (uniqueName) { + // Store the handler data. + this.sitePluginsProvider.setSitePluginHandler(uniqueName, { + plugin: plugin, + handlerName: handlerName, + handlerSchema: handlerSchema, + bootstrapResult: result + }); + } + }); + }).catch((err) => { + this.logger.error('Error executing bootstrap method', handlerSchema.bootstrap, err); + }); + } + + /** + * Given a handler in a plugin, register it in the course format delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} bootstrapResult Result of the bootstrap WS call. + * @return {string} A string to identify the handler. + */ + protected registerCourseFormatHandler(plugin: any, handlerName: string, handlerSchema: any, bootstrapResult: any): string { + this.logger.debug('Register site plugin in course format delegate:', plugin, handlerSchema, bootstrapResult); + + // Create and register the handler. + const formatName = plugin.component.replace('format_', ''); + this.courseFormatDelegate.registerHandler(new CoreSitePluginsCourseFormatHandler(formatName, handlerSchema)); + + return formatName; + } + + /** + * Given a handler in an plugin, register it in the course options delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} bootstrapResult Result of the bootstrap WS call. + * @return {string} A string to identify the handler. + */ + protected registerCourseOptionHandler(plugin: any, handlerName: string, handlerSchema: any, bootstrapResult: any): string { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in course option delegate:', plugin, handlerSchema, bootstrapResult); + + // Create and register the handler. + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title); + + this.courseOptionsDelegate.registerHandler(new CoreSitePluginsCourseOptionHandler(uniqueName, prefixedTitle, plugin, + handlerSchema, bootstrapResult, this.sitePluginsProvider)); + + return uniqueName; + } + + /** + * Given a handler in an plugin, register it in the main menu delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} bootstrapResult Result of the bootstrap WS call. + * @return {string} A string to identify the handler. + */ + protected registerMainMenuHandler(plugin: any, handlerName: string, handlerSchema: any, bootstrapResult: any): string { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in main menu delegate:', plugin, handlerSchema, bootstrapResult); + + // Create and register the handler. + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title); + + this.mainMenuDelegate.registerHandler( + new CoreSitePluginsMainMenuHandler(uniqueName, prefixedTitle, plugin, handlerSchema, bootstrapResult)); + + return uniqueName; + } + + /** + * Given a handler in an plugin, register it in the module delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} bootstrapResult Result of the bootstrap WS call. + * @return {string} A string to identify the handler. + */ + protected registerModuleHandler(plugin: any, handlerName: string, handlerSchema: any, bootstrapResult: any): string { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in module delegate:', plugin, handlerSchema, bootstrapResult); + + // Create and register the handler. + const modName = plugin.component.replace('mod_', ''); + + this.moduleDelegate.registerHandler(new CoreSitePluginsModuleHandler(modName, handlerSchema)); + + if (handlerSchema.offlinefunctions && Object.keys(handlerSchema.offlinefunctions).length) { + // Register the prefetch handler. + this.prefetchDelegate.registerHandler(new CoreSitePluginsModulePrefetchHandler( + this.injector, this.sitePluginsProvider, plugin.component, modName, handlerSchema)); + } + + return modName; + } + + /** + * Given a handler in an plugin, register it in the user profile delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} bootstrapResult Result of the bootstrap WS call. + * @return {string} A string to identify the handler. + */ + protected registerUserProfileHandler(plugin: any, handlerName: string, handlerSchema: any, bootstrapResult: any): string { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in user profile delegate:', plugin, handlerSchema, bootstrapResult); + + // Create and register the handler. + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title); + + this.userDelegate.registerHandler(new CoreSitePluginsUserProfileHandler(uniqueName, prefixedTitle, plugin, handlerSchema, + bootstrapResult, this.sitePluginsProvider)); + + return uniqueName; + } + + /** + * Given a handler in an plugin, register it in the user profile field delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} bootstrapResult Result of the bootstrap WS call. + * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. + */ + protected registerUserProfileFieldHandler(plugin: any, handlerName: string, handlerSchema: any, bootstrapResult: any) + : string | Promise { + if (!handlerSchema.method) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in user profile field delegate:', plugin, handlerSchema, bootstrapResult); + + // Execute the main method and its JS. The template returned will be used in the profile field component. + return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { + // Create and register the handler. + const fieldType = plugin.component.replace('profilefield_', ''), + fieldHandler = new CoreSitePluginsUserProfileFieldHandler(fieldType); + + // Store in handlerSchema some data required by the component. + handlerSchema.methodTemplates = result.templates; + handlerSchema.methodJSResult = result.jsResult; + + if (result && result.jsResult) { + // Override default handler functions with the result of the method JS. + for (const property in fieldHandler) { + if (property != 'constructor' && typeof fieldHandler[property] == 'function' && + typeof result.jsResult[property] == 'function') { + fieldHandler[property] = result.jsResult[property].bind(fieldHandler); + } + } + } + + this.profileFieldDelegate.registerHandler(fieldHandler); + + return fieldType; + }).catch((err) => { + this.logger.error('Error executing main method', handlerSchema.method, err); + }); + } +} diff --git a/src/core/siteplugins/providers/siteplugins.ts b/src/core/siteplugins/providers/siteplugins.ts new file mode 100644 index 000000000..b21100ba3 --- /dev/null +++ b/src/core/siteplugins/providers/siteplugins.ts @@ -0,0 +1,501 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Platform } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreLangProvider } from '@providers/lang'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreConfigConstants } from '../../../configconstants'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; + +/** + * Handler of a site plugin. + */ +export interface CoreSitePluginsHandler { + /** + * The site plugin data. + * @type {any} + */ + plugin: any; + + /** + * Name of the handler. + * @type {string} + */ + handlerName: string; + + /** + * Data of the handler. + * @type {any} + */ + handlerSchema: any; + + /** + * Result of the bootstrap WS call. + * @type {any} + */ + bootstrapResult?: any; +} + +/** + * Service to provide functionalities regarding site plugins. + */ +@Injectable() +export class CoreSitePluginsProvider { + protected ROOT_CACHE_KEY = 'CoreSitePlugins:'; + + protected logger; + protected sitePlugins: {[name: string]: CoreSitePluginsHandler} = {}; // Site plugins registered. + hasSitePluginsLoaded = false; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, + private langProvider: CoreLangProvider, private appProvider: CoreAppProvider, private platform: Platform, + private filepoolProvider: CoreFilepoolProvider, private coursesProvider: CoreCoursesProvider, + private textUtils: CoreTextUtilsProvider) { + this.logger = logger.getInstance('CoreUserProvider'); + } + + /** + * Add some params that will always be sent for get content. + * + * @param {any} args Original params. + * @param {CoreSite} [site] Site. If not defined, current site. + * @return {Promise} Promise resolved with the new params. + */ + protected addDefaultArgs(args: any, site?: CoreSite): Promise { + args = args || {}; + site = site || this.sitesProvider.getCurrentSite(); + + return this.langProvider.getCurrentLanguage().then((lang) => { + + // Clone the object so the original one isn't modified. + const argsToSend = this.utils.clone(args); + + argsToSend.userid = args.userid || site.getUserId(); + argsToSend.appid = CoreConfigConstants.app_id; + argsToSend.appversioncode = CoreConfigConstants.versioncode; + argsToSend.appversionname = CoreConfigConstants.versionname; + argsToSend.applang = lang; + argsToSend.appcustomurlscheme = CoreConfigConstants.customurlscheme; + argsToSend.appisdesktop = this.appProvider.isDesktop(); + argsToSend.appismobile = this.appProvider.isMobile(); + argsToSend.appiswide = this.appProvider.isWide(); + + if (argsToSend.appisdevice) { + if (this.platform.is('ios')) { + argsToSend.appplatform = 'ios'; + } else { + argsToSend.appplatform = 'android'; + } + } else if (argsToSend.appisdesktop) { + if (this.appProvider.isMac()) { + argsToSend.appplatform = 'mac'; + } else if (this.appProvider.isLinux()) { + argsToSend.appplatform = 'linux'; + } else { + argsToSend.appplatform = 'windows'; + } + } else { + argsToSend.appplatform = 'browser'; + } + + return argsToSend; + }); + } + + /** + * Call a WS for a site plugin. + * + * @param {string} method WS method to use. + * @param {any} data Data to send to the WS. + * @param {CoreSiteWSPreSets} [preSets] Extra options. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the response. + */ + callWS(method: string, data: any, preSets?: CoreSiteWSPreSets, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + preSets = preSets || {}; + preSets.cacheKey = preSets.cacheKey || this.getCallWSCacheKey(method, data); + + return site.read(method, data, preSets); + }); + } + + /** + * Given the result of a bootstrap get_content and, optionally, the result of another get_content, + * build an object with the data to pass to the JS of the get_content. + * + * @param {any} bootstrapResult Result of the bootstrap WS call. + * @param {any} [contentResult] Result of the content WS call (if any). + * @return {any} An object with the data to pass to the JS. + */ + createDataForJS(bootstrapResult: any, contentResult?: any): any { + // First of all, add the data returned by the bootstrap JS (if any). + let data = this.utils.clone(bootstrapResult.jsResult || {}); + if (typeof data == 'boolean') { + data = {}; + } + + // Now add some data returned by the bootstrap WS call. + data.BOOTSTRAP_TEMPLATES = this.utils.objectToKeyValueMap(bootstrapResult.templates, 'id', 'html'); + data.BOOTSTRAP_OTHERDATA = bootstrapResult.otherdata; + + if (contentResult) { + // Now add the data returned by the content WS call. + data.CONTENT_TEMPLATES = this.utils.objectToKeyValueMap(contentResult.templates, 'id', 'html'); + data.CONTENT_OTHERDATA = contentResult.otherdata; + } + + return data; + } + + /** + * Get cache key for a WS call. + * + * @param {string} method Name of the method. + * @param {any} data Data to identify the WS call. + * @return {string} Cache key. + */ + getCallWSCacheKey(method: string, data: any): string { + return this.getCallWSCommonCacheKey(method) + ':' + this.utils.sortAndStringify(data); + } + + /** + * Get common cache key for a WS call. + * + * @param {string} method Name of the method. + * @return {string} Cache key. + */ + protected getCallWSCommonCacheKey(method: string): string { + return this.ROOT_CACHE_KEY + 'ws:' + method; + } + + /** + * Get a certain content for a site plugin. + * + * @param {string} component Component where the class is. E.g. mod_assign. + * @param {string} method Method to execute in the class. + * @param {any} args The params for the method. + * @param {CoreSiteWSPreSets} [preSets] Extra options. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the result. + */ + getContent(component: string, method: string, args: any, preSets?: CoreSiteWSPreSets, siteId?: string): Promise { + this.logger.debug(`Get content for component '${component}' and method '${method}'`); + + return this.sitesProvider.getSite(siteId).then((site) => { + + // Add some params that will always be sent. + return this.addDefaultArgs(args, site).then((argsToSend) => { + // Now call the WS. + const data = { + component: component, + method: method, + args: this.utils.objectToArrayOfObjects(argsToSend, 'name', 'value', true) + }; + + preSets = preSets || {}; + preSets.cacheKey = this.getContentCacheKey(component, method, args); + + return this.sitesProvider.getCurrentSite().read('tool_mobile_get_content', data, preSets); + }).then((result) => { + if (result.otherdata) { + result.otherdata = this.utils.objectToKeyValueMap(result.otherdata, 'name', 'value'); + } else { + result.otherdata = {}; + } + + return result; + }); + }); + } + + /** + * Get cache key for get content WS calls. + * + * @param {string} component Component where the class is. E.g. mod_assign. + * @param {string} method Method to execute in the class. + * @param {any} args The params for the method. + * @return {string} Cache key. + */ + protected getContentCacheKey(component: string, method: string, args: any): string { + return this.ROOT_CACHE_KEY + 'content:' + component + ':' + method + ':' + this.utils.sortAndStringify(args); + } + + /** + * Get the value of a WS param for prefetch. + * + * @param {string} component The component of the handler. + * @param {string} paramName Name of the param as defined by the handler. + * @param {number} [courseId] Course ID (if prefetching a course). + * @param {any} [module] The module object returned by WS (if prefetching a module). + * @return {any} The value. + */ + protected getDownloadParam(component: string, paramName: string, courseId?: number, module?: any): any { + switch (paramName) { + case 'courseids': + // The WS needs the list of course IDs. Create the list. + return [courseId]; + + case component + 'id': + // The WS needs the instance id. + return module && module.instance; + + default: + // No more params supported for now. + } + } + + /** + * Get the unique name of a handler (plugin + handler). + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler inside the plugin. + * @return {string} Unique name. + */ + getHandlerUniqueName(plugin: any, handlerName: string): string { + return plugin.addon + '_' + handlerName; + } + + /** + * Get a site plugin handler. + * + * @param {string} name Unique name of the handler. + * @return {CoreSitePluginsHandler} Handler. + */ + getSitePluginHandler(name: string): CoreSitePluginsHandler { + return this.sitePlugins[name]; + } + + /** + * Invalidate all WS call to a certain method. + * + * @param {string} method WS method to use. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllCallWSForMethod(method: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getCallWSCommonCacheKey(method)); + }); + } + + /** + * Invalidate a WS call. + * + * @param {string} method WS method to use. + * @param {any} data Data to send to the WS. + * @param {CoreSiteWSPreSets} [preSets] Extra options. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateCallWS(method: string, data: any, preSets?: CoreSiteWSPreSets, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(preSets.cacheKey || this.getCallWSCacheKey(method, data)); + }); + } + + /** + * Invalidate a page content. + * + * @param {string} component Component where the class is. E.g. mod_assign. + * @param {string} method Method to execute in the class. + * @param {any} args The params for the method. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(component: string, callback: string, args: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getContentCacheKey(component, callback, args)); + }); + } + + /** + * Check if the get content WS is available. + * + * @param {CoreSite} site The site to check. If not defined, current site. + */ + isGetContentAvailable(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.wsAvailable('tool_mobile_get_content'); + } + + /** + * Check if a handler is enabled for a certain course. + * + * @param {number} courseId Course ID to check. + * @param {boolean} [restrictEnrolled] If true or undefined, handler is only enabled for courses the user is enrolled in. + * @param {any} [restrict] Users and courses the handler is restricted to. + * @return {boolean | Promise} Whether the handler is enabled. + */ + isHandlerEnabledForCourse(courseId: number, restrictEnrolled?: boolean, restrict?: any): boolean | Promise { + if (restrict && restrict.courses && restrict.courses.indexOf(courseId) == -1) { + // Course is not in the list of restricted courses. + return false; + } + + if (restrictEnrolled || typeof restrictEnrolled == 'undefined') { + // Only enabled for courses the user is enrolled to. Check if the user is enrolled in the course. + return this.coursesProvider.getUserCourse(courseId, true).then(() => { + return true; + }).catch(() => { + return false; + }); + } + + return true; + } + + /** + * Check if a handler is enabled for a certain user. + * + * @param {number} userId User ID to check. + * @param {boolean} [restrictCurrent] Whether handler is only enabled for current user. + * @param {any} [restrict] Users and courses the handler is restricted to. + * @return {boolean} Whether the handler is enabled. + */ + isHandlerEnabledForUser(userId: number, restrictCurrent?: boolean, restrict?: any): boolean { + if (restrictCurrent && userId != this.sitesProvider.getCurrentSite().getUserId()) { + // Only enabled for current user. + return false; + } + + if (restrict && restrict.users && restrict.users.indexOf(userId) == -1) { + // User is not in the list of restricted users. + return false; + } + + return true; + } + + /** + * Load other data into args as determined by useOtherData list. + * If useOtherData is undefined, it won't add any data. + * If useOtherData is defined but empty (null, false or empty string) it will copy all the data from otherData to args. + * If useOtherData is an array, it will only copy the properties whose names are in the array. + * + * @param {any} args The current args. + * @param {any} otherData All the other data. + * @param {any[]} useOtherData Names of the attributes to include. + * @return {any} New args. + */ + loadOtherDataInArgs(args: any, otherData: any, useOtherData: any[]): any { + if (!args) { + args = {}; + } else { + args = this.utils.clone(args); + } + + otherData = otherData || {}; + + if (typeof useOtherData == 'undefined') { + // No need to add other data, return args as they are. + return args; + } else if (!useOtherData) { + // Use other data is defined but empty. Add all the data to args. + for (const name in otherData) { + args[name] = otherData[name]; + } + } else { + for (const i in useOtherData) { + const name = useOtherData[i]; + args[name] = otherData[name]; + } + } + + return args; + } + + /** + * Prefetch offline functions for a site plugin handler. + * + * @param {string} component The component of the handler. + * @param {any} args Params to send to the get_content calls. + * @param {any} handlerSchema The handler schema. + * @param {number} [courseId] Course ID (if prefetching a course). + * @param {any} [module] The module object returned by WS (if prefetching a module). + * @param {boolean} [prefetch] True to prefetch, false to download right away. + * @param {string} [dirPath] Path of the directory where to store all the content files. + * @param {CoreSite} [site] Site. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetchFunctions(component: string, args: any, handlerSchema: any, courseId?: number, module?: any, prefetch?: boolean, + dirPath?: string, site?: CoreSite): Promise { + site = site || this.sitesProvider.getCurrentSite(); + + const promises = []; + + for (const method in handlerSchema.offlinefunctions) { + if (site.wsAvailable(method)) { + // The method is a WS. + const paramsList = handlerSchema.offlinefunctions[method], + cacheKey = this.getCallWSCacheKey(method, args); + let params = {}; + + if (!paramsList.length) { + // No params defined, send the default ones. + params = args; + } else { + for (const i in paramsList) { + const paramName = paramsList[i]; + + if (typeof args[paramName] != 'undefined') { + params[paramName] = args[paramName]; + } else { + // The param is not one of the default ones. Try to calculate the param to use. + const value = this.getDownloadParam(component, paramName, courseId, module); + if (typeof value != 'undefined') { + params[paramName] = value; + } + } + } + } + + promises.push(this.callWS(method, params, {cacheKey: cacheKey})); + } else { + // It's a method to get content. + promises.push(this.getContent(component, method, args).then((result) => { + const subPromises = []; + + // Prefetch the files in the content. + if (result.files && result.files.length) { + subPromises.push(this.filepoolProvider.downloadOrPrefetchFiles(site.id, result.files, prefetch, false, + component, module.id, dirPath)); + } + + return Promise.all(subPromises); + })); + } + } + + return Promise.all(promises); + } + + /** + * Store a site plugin handler. + * + * @param {string} name A unique name to identify the handler. + * @param {CoreSitePluginsHandler} handler Handler to set. + */ + setSitePluginHandler(name: string, handler: CoreSitePluginsHandler): void { + this.sitePlugins[name] = handler; + } +} diff --git a/src/core/siteplugins/siteplugins.module.ts b/src/core/siteplugins/siteplugins.module.ts new file mode 100644 index 000000000..f71eac427 --- /dev/null +++ b/src/core/siteplugins/siteplugins.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSitePluginsProvider } from './providers/siteplugins'; +import { CoreSitePluginsHelperProvider } from './providers/helper'; +import { CoreSitePluginsComponentsModule } from './components/components.module'; + +// List of providers. +export const CORE_SITEPLUGINS_PROVIDERS = [ + CoreSitePluginsProvider, + CoreSitePluginsHelperProvider +]; + +@NgModule({ + declarations: [ + ], + imports: [ + CoreSitePluginsComponentsModule + ], + providers: CORE_SITEPLUGINS_PROVIDERS +}) +export class CoreSitePluginsModule { + constructor(helper: CoreSitePluginsHelperProvider) { + // Inject the helper even if it isn't used so it's instantiated. + } +} diff --git a/src/core/user/components/components.module.ts b/src/core/user/components/components.module.ts index a996fbb8c..7e7427c17 100644 --- a/src/core/user/components/components.module.ts +++ b/src/core/user/components/components.module.ts @@ -19,8 +19,8 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreUserParticipantsComponent } from './participants/participants'; import { CoreUserProfileFieldComponent } from './user-profile-field/user-profile-field'; import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives'; -import { CorePipesModule } from '@pipes'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; @NgModule({ declarations: [ diff --git a/src/core/user/components/user-profile-field/user-profile-field.ts b/src/core/user/components/user-profile-field/user-profile-field.ts index 65b401a32..7d410d3c3 100644 --- a/src/core/user/components/user-profile-field/user-profile-field.ts +++ b/src/core/user/components/user-profile-field/user-profile-field.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, Injector } from '@angular/core'; import { CoreUserProfileFieldDelegate } from '../../providers/user-profile-field-delegate'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -33,13 +33,16 @@ export class CoreUserProfileFieldComponent implements OnInit { componentClass: any; // The class of the component to render. data: any = {}; // Data to pass to the component. - constructor(private ufDelegate: CoreUserProfileFieldDelegate, private utilsProvider: CoreUtilsProvider) { } + constructor(private ufDelegate: CoreUserProfileFieldDelegate, private utilsProvider: CoreUtilsProvider, + private injector: Injector) { } /** * Component being initialized. */ ngOnInit(): void { - this.componentClass = this.ufDelegate.getComponent(this.field, this.signup); + this.ufDelegate.getComponent(this.injector, this.field, this.signup).then((component) => { + this.componentClass = component; + }); this.data.field = this.field; this.data.edit = this.utilsProvider.isTrueOrOne(this.edit); diff --git a/src/core/user/pages/about/about.module.ts b/src/core/user/pages/about/about.module.ts index 18abe73ea..736e06435 100644 --- a/src/core/user/pages/about/about.module.ts +++ b/src/core/user/pages/about/about.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreUserAboutPage } from './about'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreComponentsModule } from '@components/components.module'; import { CoreUserComponentsModule } from '../../components/components.module'; diff --git a/src/core/user/pages/profile/profile.module.ts b/src/core/user/pages/profile/profile.module.ts index 3b7e17e5c..2511d5573 100644 --- a/src/core/user/pages/profile/profile.module.ts +++ b/src/core/user/pages/profile/profile.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreUserProfilePage } from './profile'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreComponentsModule } from '@components/components.module'; @NgModule({ diff --git a/src/core/user/providers/course-option-handler.ts b/src/core/user/providers/course-option-handler.ts index c595341b8..7de414c9e 100644 --- a/src/core/user/providers/course-option-handler.ts +++ b/src/core/user/providers/course-option-handler.ts @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { NavController } from 'ionic-angular'; -import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '../../course/providers/options-delegate'; -import { CoreCourseProvider } from '../../course/providers/course'; +import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreUserProvider } from './user'; import { CoreUserParticipantsComponent } from '../components/participants/participants'; @@ -79,9 +79,11 @@ export class CoreUserParticipantsCourseOptionHandler implements CoreCourseOption /** * Returns the data needed to render the handler. * - * @return {CoreCourseOptionsHandlerData} Data needed to render the handler. + * @param {Injector} injector Injector. + * @param {number} courseId The course ID. + * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData(): CoreCourseOptionsHandlerData { + getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { return { title: 'core.user.participants', class: 'core-user-participants-handler', diff --git a/src/core/user/providers/participants-link-handler.ts b/src/core/user/providers/participants-link-handler.ts index 2d6f1a4b7..4d4f93918 100644 --- a/src/core/user/providers/participants-link-handler.ts +++ b/src/core/user/providers/participants-link-handler.ts @@ -47,7 +47,7 @@ export class CoreUserParticipantsLinkHandler extends CoreContentLinksHandlerBase return [{ action: (siteId, navCtrl?): void => { // Always use redirect to make it the new history root (to avoid "loops" in history). - this.loginHelper.redirect('AddonParticipantsListPage', {courseId: courseId}, siteId); + this.loginHelper.redirect('CoreUserParticipantsPage', {courseId: courseId}, siteId); } }]; } diff --git a/src/core/user/providers/user-link-handler.ts b/src/core/user/providers/user-link-handler.ts index 5495105d9..faaa55c81 100644 --- a/src/core/user/providers/user-link-handler.ts +++ b/src/core/user/providers/user-link-handler.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; -import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; -import { CoreContentLinksHelperProvider } from '../../contentlinks/providers/helper'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; /** * Handler to treat links to user profiles. diff --git a/src/core/user/providers/user-profile-field-delegate.ts b/src/core/user/providers/user-profile-field-delegate.ts index 41c4b3d47..b8ba951c8 100644 --- a/src/core/user/providers/user-profile-field-delegate.ts +++ b/src/core/user/providers/user-profile-field-delegate.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; @@ -22,10 +22,12 @@ export interface CoreUserProfileFieldHandler extends CoreDelegateHandler { /** * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. * - * @return {any} The component to use, undefined if not found. + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any; + getComponent(injector: Injector): any | Promise; /** * Get the data to send for the field based on the input data. @@ -75,17 +77,23 @@ export class CoreUserProfileFieldDelegate extends CoreDelegate { /** * Get the component to use to display an user field. * + * @param {Injector} injector Injector. * @param {any} field User field to get the directive for. * @param {boolean} signup True if user is in signup page. - * @return {any} The component to use, undefined if not found. + * @return {Promise} Promise resolved with component to use, undefined if not found. */ - getComponent(field: any, signup: boolean): any { + getComponent(injector: Injector, field: any, signup: boolean): Promise { const type = field.type || field.datatype; + let result; if (signup) { - return this.executeFunction(type, 'getComponent'); + result = this.executeFunction(type, 'getComponent', [injector]); } else { - return this.executeFunctionOnEnabled(type, 'getComponent'); + result = this.executeFunctionOnEnabled(type, 'getComponent', [injector]); } + + return Promise.resolve(result).catch((err) => { + this.logger.error('Error getting component for field', type, err); + }); } /** diff --git a/src/core/user/providers/user.ts b/src/core/user/providers/user.ts index 052caa938..128f9fc93 100644 --- a/src/core/user/providers/user.ts +++ b/src/core/user/providers/user.ts @@ -310,7 +310,7 @@ export class CoreUserProvider { } /** - * Returns whether or not the participants addon is enabled for a certain course. + * Returns whether or not participants is enabled for a certain course. * * @param {number} courseId Course ID. * @param {string} [siteId] Site Id. If not defined, use current site. diff --git a/src/core/user/user.module.ts b/src/core/user/user.module.ts index bfec54d89..12d3d78df 100644 --- a/src/core/user/user.module.ts +++ b/src/core/user/user.module.ts @@ -27,22 +27,26 @@ import { CoreUserParticipantsLinkHandler } from './providers/participants-link-h import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreUserComponentsModule } from './components/components.module'; +// List of providers (without handlers). +export const CORE_USER_PROVIDERS: any[] = [ + CoreUserDelegate, + CoreUserProfileFieldDelegate, + CoreUserProvider, + CoreUserHelperProvider, +]; + @NgModule({ declarations: [ ], imports: [ CoreUserComponentsModule ], - providers: [ - CoreUserDelegate, - CoreUserProfileFieldDelegate, + providers: CORE_USER_PROVIDERS.concat([ CoreUserProfileMailHandler, - CoreUserProvider, - CoreUserHelperProvider, CoreUserProfileLinkHandler, CoreUserParticipantsCourseOptionHandler, CoreUserParticipantsLinkHandler - ] + ]) }) export class CoreUserModule { constructor(userDelegate: CoreUserDelegate, userProfileMailHandler: CoreUserProfileMailHandler, diff --git a/src/core/viewer/pages/image/image.module.ts b/src/core/viewer/pages/image/image.module.ts index 1f1a6f8e6..f3f864171 100644 --- a/src/core/viewer/pages/image/image.module.ts +++ b/src/core/viewer/pages/image/image.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreViewerImagePage } from './image'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ diff --git a/src/core/viewer/pages/text/text.module.ts b/src/core/viewer/pages/text/text.module.ts index 2a951b7d3..2cfce877b 100644 --- a/src/core/viewer/pages/text/text.module.ts +++ b/src/core/viewer/pages/text/text.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreViewerTextPage } from './text'; -import { CoreDirectivesModule } from '@directives'; +import { CoreDirectivesModule } from '@directives/directives.module'; /** * Module to lazy load the page. diff --git a/src/directives/auto-focus.ts b/src/directives/auto-focus.ts index ce2aa6608..c5f0db88e 100644 --- a/src/directives/auto-focus.ts +++ b/src/directives/auto-focus.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Directive, Input, OnInit, ElementRef } from '@angular/core'; +import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -31,7 +31,7 @@ export class CoreAutoFocusDirective implements OnInit { protected element: HTMLElement; constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider, - private navCtrl: NavController) { + @Optional() private navCtrl: NavController) { this.element = element.nativeElement || element; } diff --git a/src/directives/directives.module.ts b/src/directives/directives.module.ts index f28c00562..916710a90 100644 --- a/src/directives/directives.module.ts +++ b/src/directives/directives.module.ts @@ -14,6 +14,7 @@ import { NgModule } from '@angular/core'; import { CoreAutoFocusDirective } from './auto-focus'; +import { CoreDownloadFileDirective } from './download-file'; import { CoreExternalContentDirective } from './external-content'; import { CoreFormatTextDirective } from './format-text'; import { CoreLinkDirective } from './link'; @@ -25,6 +26,7 @@ import { CoreLongPressDirective } from './long-press'; @NgModule({ declarations: [ CoreAutoFocusDirective, + CoreDownloadFileDirective, CoreExternalContentDirective, CoreFormatTextDirective, CoreKeepKeyboardDirective, @@ -36,6 +38,7 @@ import { CoreLongPressDirective } from './long-press'; imports: [], exports: [ CoreAutoFocusDirective, + CoreDownloadFileDirective, CoreExternalContentDirective, CoreFormatTextDirective, CoreKeepKeyboardDirective, diff --git a/src/directives/download-file.ts b/src/directives/download-file.ts new file mode 100644 index 000000000..d4c4e311a --- /dev/null +++ b/src/directives/download-file.ts @@ -0,0 +1,59 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Directive, Input, OnInit, ElementRef } from '@angular/core'; +import { CoreFileHelperProvider } from '@providers/file-helper'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; + +/** + * Directive to allow downloading and open a file. When the item with this directive is clicked, the file will be + * downloaded (if needed) and opened. + */ +@Directive({ + selector: '[core-download-file]' +}) +export class CoreDownloadFileDirective implements OnInit { + @Input('core-download-file') file: any; // The file to download. + @Input() component?: string; // Component to link the file to. + @Input() componentId?: string | number; // Component ID to use in conjunction with the component. + + protected element: HTMLElement; + + constructor(element: ElementRef, protected domUtils: CoreDomUtilsProvider, protected fileHelper: CoreFileHelperProvider) { + this.element = element.nativeElement || element; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.element.addEventListener('click', (ev: Event): void => { + if (!this.file) { + return; + } + + ev.preventDefault(); + ev.stopPropagation(); + + const modal = this.domUtils.showModalLoading(); + + this.fileHelper.downloadAndOpenFile(this.file, this.component, this.componentId).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); + }).finally(() => { + modal.dismiss(); + }); + }); + } +} diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index b65a5107d..330712d1f 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -62,7 +62,7 @@ export class CoreFormatTextDirective implements OnChanges { private textUtils: CoreTextUtilsProvider, private translate: TranslateService, private platform: Platform, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private loggerProvider: CoreLoggerProvider, private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider, - private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController, + private contentLinksHelper: CoreContentLinksHelperProvider, @Optional() private navCtrl: NavController, @Optional() private content: Content) { this.element = element.nativeElement; this.element.classList.add('opacity-hide'); // Hide contents until they're treated. @@ -383,7 +383,7 @@ export class CoreFormatTextDirective implements OnChanges { return; } - const data = JSON.parse(video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}'), + const data = this.textUtils.parseJSON(video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}'), youtubeId = data.techOrder && data.techOrder[0] && data.techOrder[0] == 'youtube' && data.sources && data.sources[0] && data.sources[0].src && this.youtubeGetId(data.sources[0].src); diff --git a/src/directives/link.ts b/src/directives/link.ts index d37179cf0..75c50b186 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -39,7 +39,7 @@ export class CoreLinkDirective implements OnInit { constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider, private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider, - private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController, + private contentLinksHelper: CoreContentLinksHelperProvider, @Optional() private navCtrl: NavController, @Optional() private content: Content) { // This directive can be added dynamically. In that case, the first param is the anchor HTMLElement. this.element = element.nativeElement || element; diff --git a/src/directives/user-link.ts b/src/directives/user-link.ts index c24adb892..d1714bacb 100644 --- a/src/directives/user-link.ts +++ b/src/directives/user-link.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Directive, Input, OnInit, ElementRef } from '@angular/core'; +import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; /** @@ -27,7 +27,7 @@ export class CoreUserLinkDirective implements OnInit { protected element: HTMLElement; - constructor(element: ElementRef, private navCtrl: NavController) { + constructor(element: ElementRef, @Optional() private navCtrl: NavController) { // This directive can be added dynamically. In that case, the first param is the anchor HTMLElement. this.element = element.nativeElement || element; } diff --git a/src/providers/events.ts b/src/providers/events.ts index bbe557636..c9309ac9e 100644 --- a/src/providers/events.ts +++ b/src/providers/events.ts @@ -47,7 +47,7 @@ export class CoreEventsProvider { static PACKAGE_STATUS_CHANGED = 'package_status_changed'; static COURSE_STATUS_CHANGED = 'course_status_changed'; static SECTION_STATUS_CHANGED = 'section_status_changed'; - static REMOTE_ADDONS_LOADED = 'remote_addons_loaded'; + static SITE_PLUGINS_LOADED = 'site_plugins_loaded'; static LOGIN_SITE_CHECKED = 'login_site_checked'; static LOGIN_SITE_UNCHECKED = 'login_site_unchecked'; static IAB_LOAD_START = 'inappbrowser_load_start'; diff --git a/src/providers/file-helper.ts b/src/providers/file-helper.ts new file mode 100644 index 000000000..94315a513 --- /dev/null +++ b/src/providers/file-helper.ts @@ -0,0 +1,267 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreAppProvider } from './app'; +import { CoreFileProvider } from './file'; +import { CoreFilepoolProvider } from './filepool'; +import { CoreSitesProvider } from './sites'; +import { CoreUtilsProvider } from './utils/utils'; +import { CoreConstants } from '@core/constants'; + +/** + * Provider to provide some helper functions regarding files and packages. + */ +@Injectable() +export class CoreFileHelperProvider { + + constructor(private fileProvider: CoreFileProvider, private filepoolProvider: CoreFilepoolProvider, + private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, private translate: TranslateService, + private utils: CoreUtilsProvider) { } + + /** + * Convenience function to open a file, downloading it if needed. + * + * @param {any} file The file to download. + * @param {string} [component] The component to link the file to. + * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {string} [state] The file's state. If not provided, it will be calculated. + * @param {Function} [onProgress] Function to call on progress. + * @param {string} [siteId] The site ID. If not defined, current site. + * @return {Promise} Resolved on success. + */ + downloadAndOpenFile(file: any, component: string, componentId: string | number, state?: string, + onProgress?: (event: any) => any, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const fileUrl = this.getFileUrl(file), + timemodified = this.getFileTimemodified(file); + + return this.downloadFileIfNeeded(file, fileUrl, component, componentId, timemodified, state, onProgress, siteId) + .then((url) => { + if (!url) { + return; + } + + if (url.indexOf('http') === 0) { + return this.utils.openOnlineFile(url).catch((error) => { + // Error opening the file, some apps don't allow opening online files. + if (!this.fileProvider.isAvailable()) { + return Promise.reject(error); + } + + // Get the state. + if (state) { + return state; + } else { + return this.filepoolProvider.getFileStateByUrl(siteId, fileUrl, timemodified); + } + }).then((state) => { + if (state == CoreConstants.DOWNLOADING) { + return Promise.reject(this.translate.instant('core.erroropenfiledownloading')); + } + + let promise; + + if (state === CoreConstants.NOT_DOWNLOADED) { + // File is not downloaded, download and then return the local URL. + promise = this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); + } else { + // File is outdated and can't be opened in online, return the local URL. + promise = this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl); + } + + return promise.then((url) => { + return this.utils.openFile(url); + }); + }); + } else { + return this.utils.openFile(url); + } + }); + } + + /** + * Download a file if it needs to be downloaded. + * + * @param {any} file The file to download. + * @param {string} fileUrl The file URL. + * @param {string} [component] The component to link the file to. + * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {number} [timemodified] The time this file was modified. + * @param {string} [state] The file's state. If not provided, it will be calculated. + * @param {Function} [onProgress] Function to call on progress. + * @param {string} [siteId] The site ID. If not defined, current site. + * @return {Promise} Resolved with the URL to use on success. + */ + protected downloadFileIfNeeded(file: any, fileUrl: string, component?: string, componentId?: string | number, + timemodified?: number, state?: string, onProgress?: (event: any) => any, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.sitesProvider.getSite(siteId).then((site) => { + const fixedUrl = site.fixPluginfileURL(fileUrl); + + if (this.fileProvider.isAvailable()) { + let promise; + if (state) { + promise = Promise.resolve(state); + } else { + // Calculate the state. + promise = this.filepoolProvider.getFileStateByUrl(siteId, fileUrl, timemodified); + } + + return promise.then((state) => { + // The file system is available. + const isWifi = !this.appProvider.isNetworkAccessLimited(), + isOnline = this.appProvider.isOnline(); + + if (state == CoreConstants.DOWNLOADED) { + // File is downloaded, get the local file URL. + return this.filepoolProvider.getUrlByUrl( + siteId, fileUrl, component, componentId, timemodified, false, false, file); + } else { + if (!isOnline && !this.isStateDownloaded(state)) { + // Not downloaded and user is offline, reject. + return Promise.reject(this.translate.instant('core.networkerrormsg')); + } + + if (onProgress) { + // This call can take a while. Send a fake event to notify that we're doing some calculations. + onProgress({calculating: true}); + } + + return this.filepoolProvider.shouldDownloadBeforeOpen(fixedUrl, file.filesize).then(() => { + if (state == CoreConstants.DOWNLOADING) { + // It's already downloading, stop. + return; + } + + // Download and then return the local URL. + return this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); + }, () => { + // Start the download if in wifi, but return the URL right away so the file is opened. + if (isWifi && isOnline) { + this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); + } + + if (!this.isStateDownloaded(state) || isOnline) { + // Not downloaded or online, return the online URL. + return fixedUrl; + } else { + // Outdated but offline, so we return the local URL. + return this.filepoolProvider.getUrlByUrl( + siteId, fileUrl, component, componentId, timemodified, false, false, file); + } + }); + } + }); + } else { + // Use the online URL. + return fixedUrl; + } + }); + } + + /** + * Download the file. + * + * @param {string} fileUrl The file URL. + * @param {string} [component] The component to link the file to. + * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {number} [timemodified] The time this file was modified. + * @param {Function} [onProgress] Function to call on progress. + * @param {any} [file] The file to download. + * @param {string} [siteId] The site ID. If not defined, current site. + * @return {Promise} Resolved with internal URL on success, rejected otherwise. + */ + downloadFile(fileUrl: string, component?: string, componentId?: string | number, timemodified?: number, + onProgress?: (event: any) => any, file?: any, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Get the site and check if it can download files. + return this.sitesProvider.getSite(siteId).then((site) => { + if (!site.canDownloadFiles()) { + return Promise.reject(this.translate.instant('core.cannotdownloadfiles')); + } + + return this.filepoolProvider.downloadUrl(siteId, fileUrl, false, component, componentId, + timemodified, onProgress, undefined, file).catch((error) => { + + // Download failed, check the state again to see if the file was downloaded before. + return this.filepoolProvider.getFileStateByUrl(siteId, fileUrl, timemodified).then((state) => { + if (this.isStateDownloaded(state)) { + return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl); + } else { + return Promise.reject(error); + } + }); + }); + }); + } + + /** + * Get the file's URL. + * + * @param {any} file The file. + */ + getFileUrl(file: any): string { + return file.fileurl || file.url; + } + + /** + * Get the file's timemodified. + * + * @param {any} file The file. + */ + getFileTimemodified(file: any): number { + return file.timemodified || 0; + } + + /** + * Check if a state is downloaded or outdated. + * + * @param {string} state The state to check. + */ + isStateDownloaded(state: string): boolean { + return state === CoreConstants.DOWNLOADED || state === CoreConstants.OUTDATED; + } + + /** + * Whether the file has to be opened in browser (external repository). + * The file must have a mimetype attribute. + * + * @param {any} file The file to check. + * @return {boolean} Whether the file should be opened in browser. + */ + shouldOpenInBrowser(file: any): boolean { + if (!file || !file.isexternalfile || !file.mimetype) { + return false; + } + + const mimetype = file.mimetype; + if (mimetype.indexOf('application/vnd.google-apps.') != -1) { + // Google Docs file, always open in browser. + return true; + } + + if (file.repositorytype == 'onedrive') { + // In OneDrive, open in browser the office docs + return mimetype.indexOf('application/vnd.openxmlformats-officedocument') != -1 || + mimetype == 'text/plain' || mimetype == 'document/unknown'; + } + + return false; + } +} diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index cc677fef9..bb1ded4a5 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -961,10 +961,12 @@ export class CoreFilepoolProvider { * @param {boolean} [ignoreStale] True if 'stale' should be ignored. Only if prefetch=false. * @param {string} [component] The component to link the file to. * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {string} [dirPath] Name of the directory where to store the files (inside filepool dir). If not defined, store + * the files directly inside the filepool folder. * @return {Promise} Resolved on success. */ downloadOrPrefetchFiles(siteId: string, files: any[], prefetch: boolean, ignoreStale?: boolean, component?: string, - componentId?: string | number): Promise { + componentId?: string | number, dirPath?: string): Promise { const promises = []; // Download files. @@ -975,13 +977,23 @@ export class CoreFilepoolProvider { isexternalfile: file.isexternalfile, repositorytype: file.repositorytype }; + let path; + + if (dirPath) { + // Calculate the path to the file. + path = file.filename; + if (file.filepath !== '/') { + path = file.filepath.substr(1) + path; + } + path = this.textUtils.concatenatePaths(dirPath, path); + } if (prefetch) { promises.push(this.addToQueueByUrl( - siteId, url, component, componentId, timemodified, undefined, undefined, 0, options)); + siteId, url, component, componentId, timemodified, path, undefined, 0, options)); } else { promises.push(this.downloadUrl( - siteId, url, ignoreStale, component, componentId, timemodified, undefined, undefined, options)); + siteId, url, ignoreStale, component, componentId, timemodified, path, undefined, options)); } }); @@ -2171,7 +2183,7 @@ export class CoreFilepoolProvider { return Promise.reject(null); } // Convert the links to an object. - entry.links = JSON.parse(entry.links); + entry.links = this.textUtils.parseJSON(entry.links, []); return entry; }); @@ -2409,7 +2421,7 @@ export class CoreFilepoolProvider { return Promise.reject(this.ERR_QUEUE_IS_EMPTY); } // Convert the links to an object. - item.links = JSON.parse(item.links); + item.links = this.textUtils.parseJSON(item.links, []); return this.processQueueItem(item); }, () => { @@ -2706,7 +2718,7 @@ export class CoreFilepoolProvider { */ storePackageStatus(siteId: string, status: string, component: string, componentId?: string | number, extra?: string) : Promise { - this.logger.debug(`Set status '${status}'' for package ${component} ${componentId}`); + this.logger.debug(`Set status '${status}' for package ${component} ${componentId}`); componentId = this.fixComponentId(componentId); return this.sitesProvider.getSite(siteId).then((site) => { diff --git a/src/providers/init.ts b/src/providers/init.ts index ddb55faf1..ff27eb733 100644 --- a/src/providers/init.ts +++ b/src/providers/init.ts @@ -161,7 +161,7 @@ export class CoreInitDelegate { * * An init process should never change state or prompt user interaction. * - * This delegate cannot be used in remote addons. + * This delegate cannot be used by site plugins. * * @param {CoreInitHandler} instance The instance of the handler. */ diff --git a/src/providers/lang.ts b/src/providers/lang.ts index f8b62fae2..9f4dfd699 100644 --- a/src/providers/lang.ts +++ b/src/providers/lang.ts @@ -28,8 +28,9 @@ import { Observable } from 'rxjs'; export class CoreLangProvider { protected fallbackLanguage = CoreConfigConstants.default_lang || 'en'; protected currentLanguage: string; // Save current language in a variable to speed up the get function. - protected customStrings = {}; + protected customStrings = {}; // Strings defined using the admin tool. protected customStringsRaw: string; + protected sitePluginsStrings = {}; // Strings defined by site plugins. constructor(private translate: TranslateService, private configProvider: CoreConfigProvider, platform: Platform, private globalization: Globalization) { @@ -43,8 +44,52 @@ export class CoreLangProvider { moment.locale(language); }); }); + } - this.decorateTranslate(); + /** + * Add a set of site plugins strings for a certain language. + * + * @param {string} lang The language where to add the strings. + * @param {any} strings Object with the strings to add. + * @param {string} [prefix] A prefix to add to all keys. + */ + addSitePluginsStrings(lang: string, strings: any, prefix?: string): void { + // Initialize structures if they don't exist. + if (!this.sitePluginsStrings[lang]) { + this.sitePluginsStrings[lang] = {}; + } + if (!this.translate.translations[lang]) { + this.translate.translations[lang] = {}; + } + + for (const key in strings) { + const prefixedKey = prefix + key; + let value = strings[key]; + + if (this.customStrings[lang] && this.customStrings[lang][prefixedKey]) { + // This string is overridden by a custom string, ignore it. + continue; + } + + // Add another curly bracket to string params ({$a} -> {{$a}}). + value = value.replace(/{([^ ]+)}/gm, '{{$1}}'); + // Make sure we didn't add to many brackets in some case. + value = value.replace(/{{{([^ ]+)}}}/gm, '{{$1}}'); + + if (!this.sitePluginsStrings[lang][prefixedKey]) { + // It's a new site plugin string. Store the original value. + this.sitePluginsStrings[lang][prefixedKey] = { + original: this.translate.translations[lang][prefixedKey], + value: value + }; + } else { + // Site plugin string already defined. Store the new value. + this.sitePluginsStrings[lang][prefixedKey].value = value; + } + + // Store the string in the translations table. + this.translate.translations[lang][prefixedKey] = value; + } } /** @@ -69,51 +114,17 @@ export class CoreLangProvider { * Clear current custom strings. */ clearCustomStrings(): void { + this.unloadStrings(this.customStrings); this.customStrings = {}; this.customStringsRaw = ''; } /** - * Function to "decorate" the TranslateService. - * Basically, it extends the translate functions to use the custom lang strings. + * Clear current site plugins strings. */ - decorateTranslate(): void { - const originalGet = this.translate.get, - originalInstant = this.translate.instant; - - // Redefine translate.get. - this.translate.get = (key: string | string[], interpolateParams?: object): Observable => { - // Always call the original get function to avoid having to create our own Observables. - if (typeof key == 'string') { - const value = this.getCustomString(key); - if (typeof value != 'undefined') { - key = value; - } - } else { - key = this.getCustomStrings(key).translations; - } - - return originalGet.apply(this.translate, [key, interpolateParams]); - }; - - // Redefine translate.instant. - this.translate.instant = (key: string | string[], interpolateParams?: object): any => { - if (typeof key == 'string') { - const value = this.getCustomString(key); - if (typeof value != 'undefined') { - return value; - } - - return originalInstant.apply(this.translate, [key, interpolateParams]); - } else { - const result = this.getCustomStrings(key); - if (result.allFound) { - return result.translations; - } - - return originalInstant.apply(this.translate, [result.translations]); - } - }; + clearSitePluginsStrings(): void { + this.unloadStrings(this.sitePluginsStrings); + this.sitePluginsStrings = {}; } /** @@ -125,6 +136,15 @@ export class CoreLangProvider { return this.customStrings; } + /** + * Get all current site plugins strings. + * + * @return {any} Site plugins strings. + */ + getAllSitePluginsStrings(): any { + return this.sitePluginsStrings; + } + /** * Get current language. * @@ -174,57 +194,6 @@ export class CoreLangProvider { }); } - /** - * Get a custom string for a certain key. - * - * @param {string} key The key of the translation to get. - * @return {string} Translation, undefined if not found. - */ - getCustomString(key: string): string { - const customStrings = this.getCustomStringsForLanguage(); - if (customStrings && typeof customStrings[key] != 'undefined') { - return customStrings[key]; - } - } - - /** - * Get custom strings for several keys. - * - * @param {string[]} keys The keys of the translations to get. - * @return {any} Object with translations and a boolean indicating if all translations were found in custom strings. - */ - getCustomStrings(keys: string[]): any { - const customStrings = this.getCustomStringsForLanguage(), - translations = []; - let allFound = true; - - keys.forEach((key: string) => { - if (customStrings && typeof customStrings[key] != 'undefined') { - translations.push(customStrings[key]); - } else { - allFound = false; - translations.push(key); - } - }); - - return { - allFound: allFound, - translations: translations - }; - } - - /** - * Get custom strings for a certain language. - * - * @param {string} [lang] The language to get. If not defined, return current language. - * @return {any} Custom strings. - */ - getCustomStringsForLanguage(lang?: string): any { - lang = lang || this.currentLanguage; - - return this.customStrings[lang]; - } - /** * Load certain custom strings. * @@ -259,7 +228,36 @@ export class CoreLangProvider { this.customStrings[lang] = {}; } - this.customStrings[lang][values[0]] = values[1]; + // Store the original value of the custom string. + this.customStrings[lang][values[0]] = { + original: this.translate.translations[lang][values[0]], + value: values[1] + }; + + // Store the string in the translations table. + this.translate.translations[lang][values[0]] = values[1]; }); } + + /** + * Unload custom or site plugin strings, removing them from the translations table. + * + * @param {any} strings Strings to unload. + */ + protected unloadStrings(strings: any): void { + // Iterate over all languages and strings. + for (const lang in strings) { + const langStrings = strings[lang]; + for (const key in langStrings) { + const entry = langStrings[key]; + if (entry.original) { + // The string had a value, restore it. + this.translate.translations[lang][key] = entry.original; + } else { + // The string didn't exist, delete it. + delete this.translate.translations[lang][key]; + } + } + } + } } diff --git a/src/providers/local-notifications.ts b/src/providers/local-notifications.ts index 4bd55cfac..c3339dfcf 100644 --- a/src/providers/local-notifications.ts +++ b/src/providers/local-notifications.ts @@ -19,6 +19,7 @@ import { CoreAppProvider } from './app'; import { CoreConfigProvider } from './config'; import { CoreLoggerProvider } from './logger'; import { CoreDomUtilsProvider } from './utils/dom'; +import { CoreTextUtilsProvider } from './utils/text'; import { CoreUtilsProvider } from './utils/utils'; import { SQLiteDB } from '@classes/sqlitedb'; import { CoreConstants } from '@core/constants'; @@ -112,7 +113,7 @@ export class CoreLocalNotificationsProvider { constructor(logger: CoreLoggerProvider, private localNotifications: LocalNotifications, private platform: Platform, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private configProvider: CoreConfigProvider, - private domUtils: CoreDomUtilsProvider) { + private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider) { this.logger = logger.getInstance('CoreLocalNotificationsProvider'); this.appDB = appProvider.getDB(); this.appDB.createTablesFromSchema(this.tablesSchema); @@ -151,7 +152,7 @@ export class CoreLocalNotificationsProvider { scheduled.forEach((notif) => { if (typeof notif.data == 'string') { - notif.data = JSON.parse(notif.data); + notif.data = this.textUtils.parseJSON(notif.data); } if (typeof notif.data == 'object' && notif.data.siteId === siteId) { @@ -403,7 +404,7 @@ export class CoreLocalNotificationsProvider { notifications.forEach((notification) => { // Convert some properties to the needed types. notification.at = new Date(notification.at * 1000); - notification.data = notification.data ? JSON.parse(notification.data) : {}; + notification.data = notification.data ? this.textUtils.parseJSON(notification.data, {}) : {}; promises.push(this.scheduleNotification(notification)); }); diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 435024ee0..c4ecc8268 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -19,6 +19,7 @@ import { CoreAppProvider } from './app'; import { CoreEventsProvider } from './events'; import { CoreLoggerProvider } from './logger'; import { CoreSitesFactoryProvider } from './sites-factory'; +import { CoreTextUtilsProvider } from './utils/text'; import { CoreUrlUtilsProvider } from './utils/url'; import { CoreUtilsProvider } from './utils/utils'; import { CoreConstants } from '@core/constants'; @@ -211,7 +212,8 @@ export class CoreSitesProvider { constructor(logger: CoreLoggerProvider, private http: HttpClient, private sitesFactory: CoreSitesFactoryProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private translate: TranslateService, - private eventsProvider: CoreEventsProvider, private urlUtils: CoreUrlUtilsProvider) { + private eventsProvider: CoreEventsProvider, private urlUtils: CoreUrlUtilsProvider, + private textUtils: CoreTextUtilsProvider) { this.logger = logger.getInstance('CoreSitesProvider'); this.appDB = appProvider.getDB(); @@ -787,18 +789,9 @@ export class CoreSitesProvider { info = entry.info, config = entry.config; - // Try to parse info and config. - try { - info = info ? JSON.parse(info) : info; - } catch (ex) { - // Ignore errors. - } - - try { - config = config ? JSON.parse(config) : config; - } catch (ex) { - // Ignore errors. - } + // Parse info and config. + info = info ? this.textUtils.parseJSON(info) : info; + config = config ? this.textUtils.parseJSON(config) : config; site = this.sitesFactory.makeSite(entry.id, entry.siteUrl, entry.token, info, entry.privateToken, config, entry.loggedOut == 1); @@ -862,21 +855,15 @@ export class CoreSitesProvider { const formattedSites = []; sites.forEach((site) => { if (!ids || ids.indexOf(site.id) > -1) { - // Try to parse info. - let siteInfo = site.info; - try { - siteInfo = siteInfo ? JSON.parse(siteInfo) : siteInfo; - } catch (ex) { - // Ignore errors. - } - - const basicInfo: CoreSiteBasicInfo = { - id: site.id, - siteUrl: site.siteUrl, - fullName: siteInfo && siteInfo.fullname, - siteName: siteInfo && siteInfo.sitename, - avatar: siteInfo && siteInfo.userpictureurl - }; + // Parse info. + const siteInfo = site.info ? this.textUtils.parseJSON(site.info) : site.info, + basicInfo: CoreSiteBasicInfo = { + id: site.id, + siteUrl: site.siteUrl, + fullName: siteInfo && siteInfo.fullname, + siteName: siteInfo && siteInfo.sitename, + avatar: siteInfo && siteInfo.userpictureurl + }; formattedSites.push(basicInfo); } }); diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 72843064c..f28c8b5e7 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -264,6 +264,43 @@ export class CoreDomUtilsProvider { } } + /** + * Get the data from a form. It will only collect elements that have a name. + * + * @param {HTMLFormElement} form The form to get the data from. + * @return {any} Object with the data. The keys are the names of the inputs. + */ + getDataFromForm(form: HTMLFormElement): any { + if (!form || !form.elements) { + return {}; + } + + const data = {}; + + for (let i = 0; i < form.elements.length; i++) { + const element: any = form.elements[i], + name = element.name || ''; + + // Ignore submit inputs. + if (!name || element.type == 'submit' || element.tagName == 'BUTTON') { + return; + } + + // Get the value. + if (element.type == 'checkbox') { + data[name] = !!element.checked; + } else if (element.type == 'radio') { + if (element.checked) { + data[name] = element.value; + } + } else { + data[name] = element.value; + } + } + + return data; + } + /** * Returns height of an element. * diff --git a/src/providers/utils/text.ts b/src/providers/utils/text.ts index 11191ddaa..bb7d26c17 100644 --- a/src/providers/utils/text.ts +++ b/src/providers/utils/text.ts @@ -383,19 +383,25 @@ export class CoreTextUtilsProvider { } /** - * Same as Javascript's JSON.parse, but if an exception is thrown it will return the original text. + * Same as Javascript's JSON.parse, but it will handle errors. * * @param {string} json JSON text. + * @param {any} [defaultValue] Default value t oreturn if the parse fails. Defaults to the original value. + * @param {Function} [logErrorFn] An error to call with the exception to log the error. If not supplied, no error. * @return {any} JSON parsed as object or what it gets. */ - parseJSON(json: string): any { + parseJSON(json: string, defaultValue?: any, logErrorFn?: Function): any { try { return JSON.parse(json); } catch (ex) { - // Error, use the json text. + // Error, log the error if needed. + if (logErrorFn) { + logErrorFn(ex); + } } - return json; + // Error parsing, return the default value or the original value. + return typeof defaultValue != 'undefined' ? defaultValue : json; } /** diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index ec57e20d4..f482b279d 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -836,6 +836,10 @@ export class CoreUtilsProvider { * @return {object} Object. */ objectToKeyValueMap(objects: object[], keyName: string, valueName: string, keyPrefix?: string): object { + if (!objects) { + return; + } + const prefixSubstr = keyPrefix ? keyPrefix.length : 0, mapped = {}; objects.forEach((item) => { diff --git a/src/providers/ws.ts b/src/providers/ws.ts index 462678683..660887a06 100644 --- a/src/providers/ws.ts +++ b/src/providers/ws.ts @@ -523,6 +523,11 @@ export class CoreWSProvider { } if (typeof data.exception !== 'undefined') { + // Special debugging for site plugins, otherwise it's hard to debug errors if the data is cached. + if (method == 'tool_mobile_get_content') { + this.logger.error('Error calling WS', method, data); + } + return Promise.reject(data); } @@ -677,11 +682,7 @@ export class CoreWSProvider { } // Treat response. - try { - data = JSON.parse(data); - } catch (ex) { - // Ignore errors. - } + data = this.textUtils.parseJSON(data); // Some moodle web services return null. // If the responseExpected value is set then so long as no data is returned, we create a blank object. @@ -745,12 +746,9 @@ export class CoreWSProvider { }; return transfer.upload(filePath, uploadUrl, options, true).then((success) => { - let data: any = success.response; - try { - data = JSON.parse(data); - } catch (err) { - this.logger.error('Error parsing response from upload:', err, data); - + const data = this.textUtils.parseJSON(success.response, null, + this.logger.error.bind(this.logger, 'Error parsing response from upload')); + if (data === null) { return Promise.reject(this.translate.instant('core.errorinvalidresponse')); } diff --git a/tsconfig.json b/tsconfig.json index a6feed9c8..95c37d168 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,12 +15,13 @@ "baseUrl": "./src", "paths": { "@addon/*": ["addon/*"], + "@app/*": ["app/*"], "@classes/*": ["classes/*"], "@core/*": ["core/*"], "@providers/*": ["providers/*"], "@components/*": ["components/*"], - "@directives": ["directives/directives.module"], - "@pipes": ["pipes/pipes.module"] + "@directives/*": ["directives/*"], + "@pipes/*": ["pipes/*"] } }, "include": [