diff --git a/src/core/course/components/components.module.ts b/src/core/course/components/components.module.ts index 68401536c..ab679c5f3 100644 --- a/src/core/course/components/components.module.ts +++ b/src/core/course/components/components.module.ts @@ -22,13 +22,15 @@ import { CoreCourseFormatComponent } from './format/format'; import { CoreCourseModuleComponent } from './module/module'; import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; import { CoreCourseModuleDescriptionComponent } from './module-description/module-description'; +import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module'; @NgModule({ declarations: [ CoreCourseFormatComponent, CoreCourseModuleComponent, CoreCourseModuleCompletionComponent, - CoreCourseModuleDescriptionComponent + CoreCourseModuleDescriptionComponent, + CoreCourseUnsupportedModuleComponent ], imports: [ CommonModule, @@ -43,7 +45,11 @@ import { CoreCourseModuleDescriptionComponent } from './module-description/modul CoreCourseFormatComponent, CoreCourseModuleComponent, CoreCourseModuleCompletionComponent, - CoreCourseModuleDescriptionComponent + CoreCourseModuleDescriptionComponent, + CoreCourseUnsupportedModuleComponent + ], + entryComponents: [ + CoreCourseUnsupportedModuleComponent ] }) export class CoreCourseComponentsModule {} diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index 62ab64cba..f6ff16cdb 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -210,16 +210,17 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.componentContainers[type] = container; this.componentInstances[type] = componentRef.instance; - this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed. // Set the Input data. this.componentInstances[type].course = this.course; this.componentInstances[type].sections = this.sections; this.componentInstances[type].downloadEnabled = this.downloadEnabled; + this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed. + return true; } catch(ex) { - this.logger.error('Error creating component', type, ex, componentClass); + this.logger.error('Error creating component', type, ex); return false; } } diff --git a/src/core/course/components/unsupported-module/unsupported-module.html b/src/core/course/components/unsupported-module/unsupported-module.html new file mode 100644 index 000000000..775aff655 --- /dev/null +++ b/src/core/course/components/unsupported-module/unsupported-module.html @@ -0,0 +1,18 @@ +
+ +

{{ 'core.whoops' | translate }}

+

{{ 'core.uhoh' | translate }}

+ +

{{ 'core.course.activitydisabled' | translate }}

+

{{ 'core.course.activitynotyetviewablesiteupgradeneeded' | translate }}

+

{{ 'core.course.activitynotyetviewableremoteaddon' | translate }}

+

{{ 'core.course.askadmintosupport' | translate }}

+ +
+

{{ 'core.course.useactivityonbrowser' | translate }}

+ + {{ 'core.openinbrowser' | translate }} + + +
+
\ No newline at end of file diff --git a/src/core/course/components/unsupported-module/unsupported-module.ts b/src/core/course/components/unsupported-module/unsupported-module.ts new file mode 100644 index 000000000..1c1910d87 --- /dev/null +++ b/src/core/course/components/unsupported-module/unsupported-module.ts @@ -0,0 +1,48 @@ +// (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 } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreCourseProvider } from '../../providers/course'; +import { CoreCourseModuleDelegate } from '../../providers/module-delegate'; + +/** + * Component that displays info about an unsupported module. + */ +@Component({ + selector: 'core-course-unsupported-module', + templateUrl: 'unsupported-module.html', +}) +export class CoreCourseUnsupportedModuleComponent implements OnInit { + @Input() course: any; // The course to module belongs to. + @Input() module: any; // The module to render. + + isDisabledInSite: boolean; + isSupportedByTheApp: boolean; + moduleName: string; + + constructor(navParams: NavParams, private translate: TranslateService, private textUtils: CoreTextUtilsProvider, + private courseProvider: CoreCourseProvider, private moduleDelegate: CoreCourseModuleDelegate) {} + + /** + * Component being initialized. + */ + ngOnInit() { + this.isDisabledInSite = this.moduleDelegate.isModuleDisabledInSite(this.module.modname); + this.isSupportedByTheApp = this.moduleDelegate.hasHandler(this.module.modname); + this.moduleName = this.courseProvider.translateModuleName(this.module.modname); + } +} diff --git a/src/core/course/course.module.ts b/src/core/course/course.module.ts index 7faec0c97..489a7a128 100644 --- a/src/core/course/course.module.ts +++ b/src/core/course/course.module.ts @@ -19,12 +19,14 @@ import { CoreCourseFormatDelegate } from './providers/format-delegate'; import { CoreCourseModuleDelegate } from './providers/module-delegate'; import { CoreCourseModulePrefetchDelegate } from './providers/module-prefetch-delegate'; import { CoreCourseFormatDefaultHandler } from './providers/default-format'; +import { CoreCourseFormatSingleActivityModule } from './formats/singleactivity/singleactivity.module'; import { CoreCourseFormatTopicsModule} from './formats/topics/topics.module'; import { CoreCourseFormatWeeksModule } from './formats/weeks/weeks.module'; @NgModule({ declarations: [], imports: [ + CoreCourseFormatSingleActivityModule, CoreCourseFormatTopicsModule, CoreCourseFormatWeeksModule ], diff --git a/src/core/course/formats/singleactivity/components/format.ts b/src/core/course/formats/singleactivity/components/format.ts new file mode 100644 index 000000000..571f258f0 --- /dev/null +++ b/src/core/course/formats/singleactivity/components/format.ts @@ -0,0 +1,119 @@ +// (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, OnChanges, ViewContainerRef, ComponentFactoryResolver, ChangeDetectorRef, + SimpleChange } from '@angular/core'; +import { CoreLoggerProvider } from '../../../../../providers/logger'; +import { CoreCourseModuleDelegate } from '../../../providers/module-delegate'; +import { CoreCourseUnsupportedModuleComponent } from '../../../components/unsupported-module/unsupported-module'; + +/** + * Component to display single activity format. It will determine the right component to use and instantiate it. + * + * The instantiated component will receive the course and the module as inputs. + */ +@Component({ + selector: 'core-course-format-single-activity', + template: '' +}) +export class CoreCourseFormatSingleActivityComponent implements OnChanges { + @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. + + protected logger: any; + protected module: any; + protected componentInstance: any; + + constructor(logger: CoreLoggerProvider, private viewRef: ViewContainerRef, private factoryResolver: ComponentFactoryResolver, + private cdr: ChangeDetectorRef, private moduleDelegate: CoreCourseModuleDelegate) { + this.logger = logger.getInstance('CoreCourseFormatSingleActivityComponent'); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}) { + if (this.course && this.sections && this.sections.length) { + // In single activity the module should only have 1 section and 1 module. Get the module. + let module = this.sections[0] && this.sections[0].modules && this.sections[0].modules[0]; + if (module && !this.componentInstance) { + // We haven't created the component yet. Create it now. + this.createComponent(module); + } + + if (this.componentInstance && this.componentInstance.ngOnChanges) { + // Call ngOnChanges of the component. + let newChanges: {[name: string]: SimpleChange} = {}; + + // Check if course has changed. + if (changes.course) { + newChanges.course = changes.course + this.componentInstance.course = this.course; + } + + // Check if module has changed. + if (changes.sections && module != this.module) { + newChanges.module = { + currentValue: module, + firstChange: changes.sections.firstChange, + previousValue: this.module, + isFirstChange: () => { + return newChanges.module.firstChange; + } + }; + this.componentInstance.module = module; + this.module = module; + } + + if (Object.keys(newChanges).length) { + this.componentInstance.ngOnChanges(newChanges); + } + } + } + } + + /** + * Create the component, add it to the container and set the input data. + * + * @param {any} module The module. + * @return {boolean} Whether the component was successfully created. + */ + protected createComponent(module: any) : boolean { + let componentClass = this.moduleDelegate.getMainComponent(this.course, module) || CoreCourseUnsupportedModuleComponent; + if (!componentClass) { + // No component to instantiate. + return false; + } + + try { + // Create the component and add it to the container. + const factory = this.factoryResolver.resolveComponentFactory(componentClass), + componentRef = this.viewRef.createComponent(factory); + + this.componentInstance = componentRef.instance; + + // Set the Input data. + this.componentInstance.courseId = this.course.id; + this.componentInstance.module = module; + + // this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed. + + return true; + } catch(ex) { + this.logger.error('Error creating component', ex); + return false; + } + } +} diff --git a/src/core/course/formats/singleactivity/providers/handler.ts b/src/core/course/formats/singleactivity/providers/handler.ts new file mode 100644 index 000000000..73ff60467 --- /dev/null +++ b/src/core/course/formats/singleactivity/providers/handler.ts @@ -0,0 +1,83 @@ +// (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 { CoreCourseFormatHandler } from '../../../providers/format-delegate'; +import { CoreCourseFormatSingleActivityComponent } from '../components/format'; + +/** + * Handler to support weeks course format. + */ +@Injectable() +export class CoreCourseFormatSingleActivityHandler implements CoreCourseFormatHandler { + name = 'singleactivity'; + + constructor() {} + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled() : boolean|Promise { + return true; + } + + /** + * 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 false; + } + + /** + * Get the title to use in course page. If not defined, course fullname. + * This function will be called without sections first, and then call it again when the sections are retrieved. + * + * @param {any} course The course. + * @param {any[]} [sections] List of sections. + * @return {string} Title. + */ + getCourseTitle(course: any, sections?: any[]) : string { + if (sections && sections[0] && sections[0].modules && sections[0].modules[0]) { + return sections[0].modules[0].name; + } + return course.fullname || ''; + } + + /** + * 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 false; + } + + /** + * 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. + * + * @param {any} course The course to render. + * @return {any} The component to use, undefined if not found. + */ + getCourseFormatComponent(course: any) : any { + return CoreCourseFormatSingleActivityComponent; + } +} diff --git a/src/core/course/formats/singleactivity/singleactivity.module.ts b/src/core/course/formats/singleactivity/singleactivity.module.ts new file mode 100644 index 000000000..0ccb98388 --- /dev/null +++ b/src/core/course/formats/singleactivity/singleactivity.module.ts @@ -0,0 +1,40 @@ +// (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 { CoreCourseFormatSingleActivityComponent } from './components/format'; +import { CoreCourseFormatSingleActivityHandler } from './providers/handler'; +import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; + +@NgModule({ + declarations: [ + CoreCourseFormatSingleActivityComponent + ], + imports: [ + ], + providers: [ + CoreCourseFormatSingleActivityHandler + ], + exports: [ + CoreCourseFormatSingleActivityComponent + ], + entryComponents: [ + CoreCourseFormatSingleActivityComponent + ] +}) +export class CoreCourseFormatSingleActivityModule { + constructor(formatDelegate: CoreCourseFormatDelegate, handler: CoreCourseFormatSingleActivityHandler) { + formatDelegate.registerHandler(handler); + } +} diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index db0d61b7c..6d64c169e 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -16,9 +16,10 @@ -
- {{ 'core.course.contents' || translate }} - {{ handler.data.title || translate }} + + diff --git a/src/core/course/pages/section/section.scss b/src/core/course/pages/section/section.scss new file mode 100644 index 000000000..cbcbff326 --- /dev/null +++ b/src/core/course/pages/section/section.scss @@ -0,0 +1,24 @@ +page-core-course-section { + .core-tabs-bar { + @include position(null, null, 0, 0); + + z-index: $z-index-toolbar; + display: flex; + width: 100%; + background: $core-top-tabs-background; + + > a { + @extend .tab-button; + + background: $core-top-tabs-background; + color: $core-top-tabs-color !important; + border-bottom: 1px solid $core-top-tabs-border; + font-size: 1.6rem; + + &[aria-selected=true] { + color: $core-top-tabs-color-active !important; + border-bottom: 2px solid $core-top-tabs-color-active; + } + } + } +} diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 2bf7124e3..91ad41c80 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -58,9 +58,11 @@ export class CoreCourseSectionPage implements OnDestroy { private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider, sitesProvider: CoreSitesProvider) { this.course = navParams.get('course'); - this.title = courseFormatDelegate.getCourseTitle(this.course); this.moduleId = navParams.get('moduleId'); + // Get the title to display. We dont't have sections yet. + this.title = courseFormatDelegate.getCourseTitle(this.course); + this.completionObserver = eventsProvider.on(CoreEventsProvider.COMPLETION_MODULE_VIEWED, (data) => { if (data && data.courseId == this.course.id) { this.refreshAfterCompletionChange(); @@ -150,6 +152,9 @@ export class CoreCourseSectionPage implements OnDestroy { id: CoreCourseProvider.ALL_SECTIONS_ID }); } + + // Get the title again now that we have sections. + this.title = this.courseFormatDelegate.getCourseTitle(this.course, this.sections); })); })); diff --git a/src/core/course/pages/unsupported-module/unsupported-module.html b/src/core/course/pages/unsupported-module/unsupported-module.html index 32a4c3b9f..8dbdd656d 100644 --- a/src/core/course/pages/unsupported-module/unsupported-module.html +++ b/src/core/course/pages/unsupported-module/unsupported-module.html @@ -10,21 +10,6 @@ - - -

{{ 'core.whoops' | translate }}

-

{{ 'core.uhoh' | translate }}

- -

{{ 'core.course.activitydisabled' | translate }}

-

{{ 'core.course.activitynotyetviewablesiteupgradeneeded' | translate }}

-

{{ 'core.course.activitynotyetviewableremoteaddon' | translate }}

-

{{ 'core.course.askadmintosupport' | translate }}

- -
-

{{ 'core.course.useactivityonbrowser' | translate }}

- - {{ 'core.openinbrowser' | translate }} - - -
+ + diff --git a/src/core/course/pages/unsupported-module/unsupported-module.ts b/src/core/course/pages/unsupported-module/unsupported-module.ts index 3489c7128..0ed57eb2c 100644 --- a/src/core/course/pages/unsupported-module/unsupported-module.ts +++ b/src/core/course/pages/unsupported-module/unsupported-module.ts @@ -13,11 +13,9 @@ // limitations under the License. import { Component } from '@angular/core'; -import { IonicPage, NavParams } from 'ionic-angular'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; -import { CoreCourseProvider } from '../../providers/course'; -import { CoreCourseModuleDelegate } from '../../providers/module-delegate'; /** * Page that displays info about an unsupported module. @@ -28,30 +26,18 @@ import { CoreCourseModuleDelegate } from '../../providers/module-delegate'; templateUrl: 'unsupported-module.html', }) export class CoreCourseUnsupportedModulePage { - module: any; - isDisabledInSite: boolean; - isSupportedByTheApp: boolean; - moduleName: string; constructor(navParams: NavParams, private translate: TranslateService, private textUtils: CoreTextUtilsProvider, - private moduleDelegate: CoreCourseModuleDelegate, private courseProvider: CoreCourseProvider) { + private navCtrl: NavController) { this.module = navParams.get('module') || {}; } - /** - * View loaded. - */ - ionViewDidLoad() { - this.isDisabledInSite = this.moduleDelegate.isModuleDisabledInSite(this.module.modname); - this.isSupportedByTheApp = this.moduleDelegate.hasHandler(this.module.modname); - this.moduleName = this.courseProvider.translateModuleName(this.module.modname); - } - /** * Expand the description. */ expandDescription() { - this.textUtils.expandText(this.translate.instant('core.description'), this.module.description, false); + this.textUtils.expandText(this.translate.instant('core.description'), this.module.description, false, + undefined, undefined, this.navCtrl); } } diff --git a/src/core/course/providers/format-delegate.ts b/src/core/course/providers/format-delegate.ts index 80d077213..36db3a662 100644 --- a/src/core/course/providers/format-delegate.ts +++ b/src/core/course/providers/format-delegate.ts @@ -39,11 +39,13 @@ export interface CoreCourseFormatHandler { /** * Get the title to use in course page. If not defined, course fullname. + * This function will be called without sections first, and then call it again when the sections are retrieved. * * @param {any} course The course. + * @param {any[]} [sections] List of sections. * @return {string} Title. */ - getCourseTitle?(course: any) : string; + getCourseTitle?(course: any, sections?: any[]) : string; /** * Whether it allows seeing all sections at the same time. Defaults to true. @@ -227,10 +229,11 @@ export class CoreCourseFormatDelegate { * Given a course, return the title to use in the course page. * * @param {any} course The course to get the title. + * @param {any[]} [sections] List of sections. * @return {string} Course title. */ - getCourseTitle(course: any) : string { - return this.executeFunction(course.format, 'getCourseTitle', [course]); + getCourseTitle(course: any, sections?: any[]) : string { + return this.executeFunction(course.format, 'getCourseTitle', [course, sections]); } /** diff --git a/src/core/course/providers/module-delegate.ts b/src/core/course/providers/module-delegate.ts index f36d5c8db..6dd1a7700 100644 --- a/src/core/course/providers/module-delegate.ts +++ b/src/core/course/providers/module-delegate.ts @@ -52,6 +52,15 @@ export interface CoreCourseModuleHandler { * @return {CoreCourseModuleHandlerData} Data to render the module. */ getData(module: any, courseId: number, sectionId: number) : CoreCourseModuleHandlerData; + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * + * @param {any} course The course object. + * @param {any} module The module object. + * @return {any} The component to use, undefined if not found. + */ + getMainComponent(course: any, module: any) : any; }; /** @@ -163,6 +172,23 @@ export class CoreCourseModuleDelegate { eventsProvider.on(CoreEventsProvider.REMOTE_ADDONS_LOADED, this.updateHandlers.bind(this)); } + /** + * Get the component to render the module. + * + * @param {any} course The course object. + * @param {any} module The module object. + * @return {any} The component to use, undefined if not found. + */ + getMainComponent?(course: any, module: any) : any { + let handler = this.enabledHandlers[module.modname]; + if (handler && handler.getMainComponent) { + let component = handler.getMainComponent(course, module); + if (component) { + return component; + } + } + } + /** * Get the data required to display the module in the course contents view. * diff --git a/src/core/fileuploader/providers/delegate.ts b/src/core/fileuploader/providers/delegate.ts index 7cedfcb50..f266ea6e1 100644 --- a/src/core/fileuploader/providers/delegate.ts +++ b/src/core/fileuploader/providers/delegate.ts @@ -165,7 +165,7 @@ export class CoreFileUploaderDelegate { protected lastUpdateHandlersStart: number; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) { - this.logger = logger.getInstance('CoreCourseModuleDelegate'); + this.logger = logger.getInstance('CoreFileUploaderDelegate'); eventsProvider.on(CoreEventsProvider.LOGIN, this.updateHandlers.bind(this)); eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.updateHandlers.bind(this)); diff --git a/src/providers/utils/text.ts b/src/providers/utils/text.ts index 587f2f1fd..6880abe56 100644 --- a/src/providers/utils/text.ts +++ b/src/providers/utils/text.ts @@ -251,7 +251,7 @@ export class CoreTextUtilsProvider { * @param {boolean} [isModal] Whether it should be opened in a modal (true) or in a new page (false). * @param {string} [component] Component to link the embedded files to. * @param {string|number} [componentId] An ID to use in conjunction with the component. - * @param {NavController} [navCtrl] The NavController instance to use. + * @param {NavController} [navCtrl] The NavController instance to use. Required if isModal is false. */ expandText(title: string, text: string, isModal?: boolean, component?: string, componentId?: string|number, navCtrl?: NavController) : void {