diff --git a/src/addon/mod/label/label.module.ts b/src/addon/mod/label/label.module.ts new file mode 100644 index 000000000..f35ee2eaa --- /dev/null +++ b/src/addon/mod/label/label.module.ts @@ -0,0 +1,37 @@ +// (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 { AddonModLabelModuleHandler } from './providers/module-handler'; +import { AddonModLabelLinkHandler } from './providers/link-handler'; +import { CoreCourseModuleDelegate } from '../../../core/course/providers/module-delegate'; +import { CoreContentLinksDelegate } from '../../../core/contentlinks/providers/delegate'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + AddonModLabelModuleHandler, + AddonModLabelLinkHandler + ] +}) +export class AddonModLabelModule { + constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModLabelModuleHandler, + contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModLabelLinkHandler) { + moduleDelegate.registerHandler(moduleHandler); + contentLinksDelegate.registerHandler(linkHandler); + } +} diff --git a/src/addon/mod/label/providers/link-handler.ts b/src/addon/mod/label/providers/link-handler.ts new file mode 100644 index 000000000..5dc547d8c --- /dev/null +++ b/src/addon/mod/label/providers/link-handler.ts @@ -0,0 +1,29 @@ +// (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 { CoreContentLinksModuleIndexHandler } from '../../../../core/contentlinks/classes/module-index-handler'; +import { CoreCourseHelperProvider } from '../../../../core/course/providers/helper'; + +/** + * Handler to treat links to label. + */ +@Injectable() +export class AddonModLabelLinkHandler extends CoreContentLinksModuleIndexHandler { + name = 'AddonModLabelLinkHandler'; + + constructor(courseHelper: CoreCourseHelperProvider) { + super(courseHelper, 'mmaModLabel', 'label'); + } +} diff --git a/src/addon/mod/label/providers/module-handler.ts b/src/addon/mod/label/providers/module-handler.ts new file mode 100644 index 000000000..a32b9474a --- /dev/null +++ b/src/addon/mod/label/providers/module-handler.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 { Injectable } from '@angular/core'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '../../../../core/course/providers/module-delegate'; + +/** + * Handler to support label modules. + */ +@Injectable() +export class AddonModLabelModuleHandler implements CoreCourseModuleHandler { + name = 'label'; + + constructor() { + // Nothing to do. + } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * 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 { + // Remove the description from the module so it isn't rendered twice. + const title = module.description; + module.description = ''; + + return { + icon: '', + title: title, + class: 'addon-mod-label-handler' + }; + } + + /** + * 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 { + // There's no need to implement this because label cannot be used in singleactivity course format. + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d96ef144c..5da362ffc 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -66,6 +66,7 @@ import { CoreUserModule } from '../core/user/user.module'; import { AddonCalendarModule } from '../addon/calendar/calendar.module'; import { AddonUserProfileFieldModule } from '../addon/userprofilefield/userprofilefield.module'; import { AddonFilesModule } from '../addon/files/files.module'; +import { AddonModLabelModule } from '../addon/mod/label/label.module'; // For translate loader. AoT requires an exported function for factories. export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { @@ -103,7 +104,8 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { CoreUserModule, AddonCalendarModule, AddonUserProfileFieldModule, - AddonFilesModule + AddonFilesModule, + AddonModLabelModule ], bootstrap: [IonicApp], entryComponents: [ diff --git a/src/core/contentlinks/classes/module-grade-handler.ts b/src/core/contentlinks/classes/module-grade-handler.ts index 89b744cd2..edf7bcac0 100644 --- a/src/core/contentlinks/classes/module-grade-handler.ts +++ b/src/core/contentlinks/classes/module-grade-handler.ts @@ -24,31 +24,28 @@ import { CoreCourseHelperProvider } from '../../course/providers/helper'; */ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerBase { - /** - * Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled. - * @type {string} - */ - addon: string; - - /** - * Name of the module (assign, book, ...). - * @type {string} - */ - modName: string; - /** * Whether the module can be reviewed in the app. If true, the handler needs to implement the goToReview function. * @type {boolean} */ canReview: boolean; + /** + * Construct the handler. + * + * @param {CoreCourseHelperProvider} courseHelper The CoreCourseHelperProvider instance. + * @param {CoreDomUtilsProvider} domUtils The CoreDomUtilsProvider instance. + * @param {CoreSitesProvider} sitesProvider The CoreSitesProvider instance. + * @param {string} addon Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled. + * @param {string} modName Name of the module (assign, book, ...). + */ constructor(protected courseHelper: CoreCourseHelperProvider, protected domUtils: CoreDomUtilsProvider, - protected sitesProvider: CoreSitesProvider) { + protected sitesProvider: CoreSitesProvider, public addon: string, public modName: string) { super(); // Match the grade.php URL with an id param. - this.pattern = new RegExp('\/mod\/' + this.modName + '\/grade\.php.*([\&\?]id=\\d+)'); - this.featureName = '$mmCourseDelegate_' + this.addon; + this.pattern = new RegExp('\/mod\/' + modName + '\/grade\.php.*([\&\?]id=\\d+)'); + this.featureName = '$mmCourseDelegate_' + addon; } /** diff --git a/src/core/contentlinks/classes/module-index-handler.ts b/src/core/contentlinks/classes/module-index-handler.ts index f7058468b..32fb2007a 100644 --- a/src/core/contentlinks/classes/module-index-handler.ts +++ b/src/core/contentlinks/classes/module-index-handler.ts @@ -22,23 +22,18 @@ import { CoreCourseHelperProvider } from '../../course/providers/helper'; export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerBase { /** - * Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled. - * @type {string} + * Construct the handler. + * + * @param {CoreCourseHelperProvider} courseHelper The CoreCourseHelperProvider instance. + * @param {string} addon Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled. + * @param {string} modName Name of the module (assign, book, ...). */ - addon: string; - - /** - * Name of the module (assign, book, ...). - * @type {string} - */ - modName: string; - - constructor(private courseHelper: CoreCourseHelperProvider) { + constructor(protected courseHelper: CoreCourseHelperProvider, public addon: string, public modName: string) { super(); // Match the view.php URL with an id param. - this.pattern = new RegExp('\/mod\/' + this.modName + '\/view\.php.*([\&\?]id=\\d+)'); - this.featureName = '$mmCourseDelegate_' + this.addon; + this.pattern = new RegExp('\/mod\/' + modName + '\/view\.php.*([\&\?]id=\\d+)'); + this.featureName = '$mmCourseDelegate_' + addon; } /** diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index 578f9fe0f..999c78ad1 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter } from '@angular/core'; +import { Content } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '../../../../providers/events'; import { CoreSitesProvider } from '../../../../providers/sites'; @@ -42,6 +43,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. @Input() initialSectionId?: number; // The section to load first (by ID). @Input() initialSectionNumber?: number; // The section to load first (by number). + @Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section. @Output() completionChanged?: EventEmitter; // Will emit an event when any module completion changes. // All the possible component classes. @@ -64,7 +66,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider, - eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider, + eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider, private content: Content, prefetchDelegate: CoreCourseModulePrefetchDelegate) { this.selectOptions.title = translate.instant('core.course.sections'); @@ -132,7 +134,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { // We have an input indicating the section ID to load. Search the section. for (let i = 0; i < this.sections.length; i++) { const section = this.sections[i]; - if (section.id == this.initialSectionId || section.section == this.initialSectionNumber) { + if ((section.id && section.id == this.initialSectionId) || + (section.section && section.section == this.initialSectionNumber)) { this.loaded = true; this.sectionChanged(section); break; @@ -212,6 +215,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { const previousValue = this.selectedSection; this.selectedSection = newSection; this.data.section = this.selectedSection; + + if (this.moduleId && typeof previousValue == 'undefined') { + setTimeout(() => { + this.domUtils.scrollToElementBySelector(this.content, '#core-course-module-' + this.moduleId); + }, 200); + } } /** diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index b57f35177..2360f470f 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -21,7 +21,7 @@ - + diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 06dd8666a..35a434feb 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -50,12 +50,14 @@ export class CoreCourseSectionPage implements OnDestroy { prefetchCourseData = { prefetchCourseIcon: 'spinner' }; + moduleId: number; + protected module: any; protected completionObserver; protected courseStatusObserver; protected isDestroyed = false; - constructor(private navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, + constructor(navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, private courseFormatDelegate: CoreCourseFormatDelegate, private courseOptionsDelegate: CoreCourseOptionsDelegate, private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider, @@ -64,6 +66,7 @@ export class CoreCourseSectionPage implements OnDestroy { 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. @@ -88,9 +91,9 @@ export class CoreCourseSectionPage implements OnDestroy { */ ionViewDidLoad(): void { - const module = this.navParams.get('module'); - if (module) { - this.courseHelper.openModule(this.navCtrl, module, this.course.id, this.sectionId); + if (this.module) { + this.moduleId = this.module.id; + this.courseHelper.openModule(this.navCtrl, this.module, this.course.id, this.sectionId); } this.loadData().finally(() => { diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 8eee00931..d4b76e357 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -641,13 +641,20 @@ export class CoreCourseHelperProvider { * @param {any} module The module to open. * @param {number} courseId The course ID of the module. * @param {number} [sectionId] The section ID of the module. + * @param {boolean} True if module can be opened, false otherwise. */ - openModule(navCtrl: NavController, module: any, courseId: number, sectionId?: number): void { + openModule(navCtrl: NavController, module: any, courseId: number, sectionId?: number): boolean { if (!module.handlerData) { module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, sectionId); } - module.handlerData.action(new Event('click'), navCtrl, module, courseId, { animate: false }); + if (module.handlerData && module.handlerData.action) { + module.handlerData.action(new Event('click'), navCtrl, module, courseId, { animate: false }); + + return true; + } + + return false; } /** diff --git a/src/core/login/pages/email-signup/email-signup.ts b/src/core/login/pages/email-signup/email-signup.ts index 6b3ed9172..cbe3349b5 100644 --- a/src/core/login/pages/email-signup/email-signup.ts +++ b/src/core/login/pages/email-signup/email-signup.ts @@ -205,7 +205,7 @@ export class CoreLoginEmailSignupPage { create(): void { if (!this.signupForm.valid) { // Form not valid. Scroll to the first element with errors. - if (!this.domUtils.scrollToInputError(this.content, document.body)) { + if (!this.domUtils.scrollToInputError(this.content)) { // Input not found, show an error modal. this.domUtils.showErrorModal('core.errorinvalidform', true); } diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index cf492bd2d..d48e0f6a8 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; -import { Platform, NavController } from 'ionic-angular'; +import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core'; +import { Platform, NavController, Content } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '../providers/app'; import { CoreFilepoolProvider } from '../providers/filepool'; @@ -62,7 +62,8 @@ 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, private navCtrl: NavController, + @Optional() private content: Content) { this.element = element.nativeElement; this.element.classList.add('opacity-hide'); // Hide contents until they're treated. this.afterRender = new EventEmitter(); @@ -280,7 +281,7 @@ export class CoreFormatTextDirective implements OnChanges { anchors.forEach((anchor) => { // Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually. const linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils, - this.contentLinksHelper, this.navCtrl); + this.contentLinksHelper, this.navCtrl, this.content); linkDir.capture = true; linkDir.ngOnInit(); diff --git a/src/directives/link.ts b/src/directives/link.ts index 2a54f67b7..d565a90c5 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Directive, Input, OnInit, ElementRef } from '@angular/core'; -import { NavController } from 'ionic-angular'; +import { NavController, Content } from 'ionic-angular'; import { CoreSitesProvider } from '../providers/sites'; import { CoreDomUtilsProvider } from '../providers/utils/dom'; import { CoreUrlUtilsProvider } from '../providers/utils/url'; @@ -38,8 +38,9 @@ export class CoreLinkDirective implements OnInit { protected element: HTMLElement; constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider, - private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider, - private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController) { + private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider, + private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController, + private content: Content) { // This directive can be added dynamically. In that case, the first param is the anchor HTMLElement. this.element = element.nativeElement || element; } @@ -93,8 +94,7 @@ export class CoreLinkDirective implements OnInit { // $location.url(href); } else { // Look for id or name. - const scrollEl = this.domUtils.closest(this.element, 'scroll-content'); - this.domUtils.scrollToElement(scrollEl, document.body, '#' + href + ', [name=\'' + href + '\']'); + this.domUtils.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']'); } } else if (href.indexOf(contentLinksScheme) === 0) { // Link should be treated by Custom URL Scheme. Encode the right part, otherwise ':' is removed in iOS. diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 8d87fc766..e1a8a5c41 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -584,22 +584,39 @@ export class CoreDomUtilsProvider { } /** - * Scroll to a certain element inside another element. + * Scroll to a certain element. * - * @param {Content|HTMLElement} scrollEl The content that must be scrolled. - * @param {HTMLElement} container Element to search in. - * @param {string} [selector] Selector to find the element to scroll to. If not defined, scroll to the container. + * @param {Content} content The content that must be scrolled. + * @param {HTMLElement} element The element to scroll to. * @param {string} [scrollParentClass] Parent class where to stop calculating the position. Default scroll-content. * @return {boolean} True if the element is found, false otherwise. */ - scrollToElement(scrollEl: Content | HTMLElement, container: HTMLElement, selector?: string, scrollParentClass?: string) - : boolean { - const position = this.getElementXY(container, selector, scrollParentClass); + scrollToElement(content: Content, element: HTMLElement, scrollParentClass?: string): boolean { + const position = this.getElementXY(element, undefined, scrollParentClass); if (!position) { return false; } - scrollEl.scrollTo(position[0], position[1]); + content.scrollTo(position[0], position[1]); + + return true; + } + + /** + * Scroll to a certain element using a selector to find it. + * + * @param {Content} content The content that must be scrolled. + * @param {string} selector Selector to find the element to scroll to. + * @param {string} [scrollParentClass] Parent class where to stop calculating the position. Default scroll-content. + * @return {boolean} True if the element is found, false otherwise. + */ + scrollToElementBySelector(content: Content, selector: string, scrollParentClass?: string): boolean { + const position = this.getElementXY(content.getScrollElement(), selector, scrollParentClass); + if (!position) { + return false; + } + + content.scrollTo(position[0], position[1]); return true; } @@ -607,17 +624,16 @@ export class CoreDomUtilsProvider { /** * Search for an input with error (core-input-error directive) and scrolls to it if found. * - * @param {Content|HTMLElement} scrollEl The element that must be scrolled. - * @param {HTMLElement} container Element to search in. + * @param {Content} content The content that must be scrolled. * @param [scrollParentClass] Parent class where to stop calculating the position. Default scroll-content. * @return {boolean} True if the element is found, false otherwise. */ - scrollToInputError(scrollEl: Content | HTMLElement, container: HTMLElement, scrollParentClass?: string): boolean { - if (!scrollEl) { + scrollToInputError(content: Content, scrollParentClass?: string): boolean { + if (!content) { return false; } - return this.scrollToElement(scrollEl, container, '.core-input-error', scrollParentClass); + return this.scrollToElementBySelector(content, '.core-input-error', scrollParentClass); } /**