diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index 2f47b9f38..ea12ab46f 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -15,6 +15,7 @@ + diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 7246d3fd9..2ef55918c 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -24,7 +24,8 @@ import { CoreCourseProvider } from '../../providers/course'; import { CoreCourseHelperProvider } from '../../providers/helper'; import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; import { CoreCourseModulePrefetchDelegate } from '../../providers/module-prefetch-delegate'; -import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay } from '../../providers/options-delegate'; +import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay, + CoreCourseOptionsMenuHandlerToDisplay } from '../../providers/options-delegate'; import { CoreCourseSyncProvider } from '../../providers/sync'; import { CoreCourseFormatComponent } from '../../components/format/format'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; @@ -49,6 +50,7 @@ export class CoreCourseSectionPage implements OnDestroy { sectionId: number; sectionNumber: number; courseHandlers: CoreCourseOptionsHandlerToDisplay[]; + courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = []; dataLoaded: boolean; downloadEnabled: boolean; downloadEnabledIcon = 'square-outline'; // Disabled by default. @@ -301,6 +303,11 @@ export class CoreCourseSectionPage implements OnDestroy { } })); + // Load the course menu handlers. + promises.push(this.courseOptionsDelegate.getMenuHandlersToDisplay(this.injector, this.course).then((handlers) => { + this.courseMenuHandlers = handlers; + })); + // Load the course format options when course completion is enabled to show completion progress on sections. if (this.course.enablecompletion && this.coursesProvider.isGetCoursesByFieldAvailable()) { promises.push(this.coursesProvider.getCoursesByField('id', this.course.id).catch(() => { @@ -417,7 +424,8 @@ export class CoreCourseSectionPage implements OnDestroy { * Prefetch the whole course. */ prefetchCourse(): void { - this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course, this.sections, this.courseHandlers) + this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course, this.sections, + this.courseHandlers, this.courseMenuHandlers) .then(() => { if (this.downloadEnabled) { // Recalculate the status. @@ -459,6 +467,16 @@ export class CoreCourseSectionPage implements OnDestroy { this.navCtrl.push('CoreCoursesCoursePreviewPage', {course: this.course, avoidOpenCourse: true}); } + /** + * Opens a menu item registered to the delegate. + * + * @param {CoreCourseMenuHandlerToDisplay} item Item to open + */ + openMenuItem(item: CoreCourseOptionsMenuHandlerToDisplay): void { + const params = Object.assign({ course: this.course}, item.data.pageParams); + this.navCtrl.push(item.data.page, params); + } + /** * Page destroyed. */ diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index aa57300b6..e137ac30c 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -25,7 +25,8 @@ 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 { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay, + CoreCourseOptionsMenuHandlerToDisplay } from './options-delegate'; import { CoreSiteHomeProvider } from '@core/sitehome/providers/sitehome'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCourseProvider } from './course'; @@ -273,10 +274,11 @@ export class CoreCourseHelperProvider { * @param {any} course Course to prefetch. * @param {any[]} [sections] List of course sections. * @param {CoreCourseOptionsHandlerToDisplay[]} courseHandlers List of course handlers. + * @param {CoreCourseOptionsMenuHandlerToDisplay[]} menuHandlers List of course menu handlers. * @return {Promise} Promise resolved when the download finishes, rejected if an error occurs or the user cancels. */ - confirmAndPrefetchCourse(data: any, course: any, sections?: any[], courseHandlers?: CoreCourseOptionsHandlerToDisplay[]) - : Promise { + confirmAndPrefetchCourse(data: any, course: any, sections?: any[], courseHandlers?: CoreCourseOptionsHandlerToDisplay[], + menuHandlers?: CoreCourseOptionsMenuHandlerToDisplay[]): Promise { const initialIcon = data.prefetchCourseIcon, initialTitle = data.title, @@ -297,15 +299,23 @@ export class CoreCourseHelperProvider { // Confirm the download. return this.confirmDownloadSizeSection(course.id, undefined, sections, true).then(() => { // User confirmed, get the course handlers if needed. - if (courseHandlers) { - promise = Promise.resolve(courseHandlers); - } else { - promise = this.courseOptionsDelegate.getHandlersToDisplay(this.injector, course); + const subPromises = []; + if (!courseHandlers) { + subPromises.push(this.courseOptionsDelegate.getHandlersToDisplay(this.injector, course) + .then((cHandlers) => { + courseHandlers = cHandlers; + })); + } + if (!menuHandlers) { + subPromises.push(this.courseOptionsDelegate.getMenuHandlersToDisplay(this.injector, course) + .then((mHandlers) => { + menuHandlers = mHandlers; + })); } - return promise.then((handlers: CoreCourseOptionsHandlerToDisplay[]) => { + return Promise.all(subPromises).then(() => { // Now we have all the data, download the course. - return this.prefetchCourse(course, sections, handlers, siteId); + return this.prefetchCourse(course, sections, courseHandlers, menuHandlers, siteId); }).then(() => { // Download successful. return true; @@ -340,6 +350,7 @@ export class CoreCourseHelperProvider { const subPromises = []; let sections, handlers, + menuHandlers, success = true; // Get the sections and the handlers. @@ -349,9 +360,12 @@ export class CoreCourseHelperProvider { subPromises.push(this.courseOptionsDelegate.getHandlersToDisplay(this.injector, course).then((cHandlers) => { handlers = cHandlers; })); + subPromises.push(this.courseOptionsDelegate.getMenuHandlersToDisplay(this.injector, course).then((mHandlers) => { + menuHandlers = mHandlers; + })); promises.push(Promise.all(subPromises).then(() => { - return this.prefetchCourse(course, sections, handlers, siteId); + return this.prefetchCourse(course, sections, handlers, menuHandlers, siteId); }).catch((error) => { success = false; @@ -1162,11 +1176,12 @@ export class CoreCourseHelperProvider { * @param {any} course The course to prefetch. * @param {any[]} sections List of course sections. * @param {CoreCourseOptionsHandlerToDisplay[]} courseHandlers List of course options handlers. + * @param {CoreCourseOptionsMenuHandlerToDisplay[]} courseMenuHandlers List of course menu handlers. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the download finishes. */ - prefetchCourse(course: any, sections: any[], courseHandlers: CoreCourseOptionsHandlerToDisplay[], siteId?: string) - : Promise { + prefetchCourse(course: any, sections: any[], courseHandlers: CoreCourseOptionsHandlerToDisplay[], + courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[], siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (this.courseDwnPromises[siteId] && this.courseDwnPromises[siteId][course.id]) { @@ -1195,6 +1210,11 @@ export class CoreCourseHelperProvider { promises.push(handler.prefetch(course)); } }); + courseMenuHandlers.forEach((handler) => { + if (handler.prefetch) { + promises.push(handler.prefetch(course)); + } + }); // Prefetch other data needed to render the course. if (this.coursesProvider.isGetCoursesByFieldAvailable()) { diff --git a/src/core/course/providers/options-delegate.ts b/src/core/course/providers/options-delegate.ts index 961c61939..cccfde281 100644 --- a/src/core/course/providers/options-delegate.ts +++ b/src/core/course/providers/options-delegate.ts @@ -31,6 +31,12 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler { */ priority: number; + /** + * True if this handler should appear in menu rather than as a tab. + * @type {boolean} + */ + isMenuHandler?: boolean; + /** * Whether or not the handler is enabled for a certain course. * @@ -70,6 +76,21 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler { prefetch?(course: any): Promise; } +/** + * Interface that course options handlers implement if they appear in the menu rather than as a tab. + */ +export interface CoreCourseOptionsMenuHandler extends CoreCourseOptionsHandler { + /** + * Returns the data needed to render the handler. + * + * @param {Injector} injector Injector. + * @param {number} courseId The course ID. + * @return {CoreCourseOptionsMenuHandlerData|Promise} Data or promise resolved with data. + */ + getMenuDisplayData(injector: Injector, courseId: number): + CoreCourseOptionsMenuHandlerData | Promise; +} + /** * Data needed to render a course handler. It's returned by the handler. */ @@ -99,6 +120,41 @@ export interface CoreCourseOptionsHandlerData { componentData?: any; } +/** + * Data needed to render a course menu handler. It's returned by the handler. + */ +export interface CoreCourseOptionsMenuHandlerData { + /** + * Title to display for the handler. + * @type {string} + */ + title: string; + + /** + * Class to add to the displayed handler. + * @type {string} + */ + class?: string; + + /** + * Name of the page to load for the handler. + * @type {string} + */ + page: string; + + /** + * Params to pass to the page (other than 'course' which is always sent). + * @type {any} + */ + pageParams?: any; + + /** + * Name of the icon to display for the handler. + * @type {string} + */ + icon: string; // Name of the icon to display in the tab. +} + /** * Data returned by the delegate for each handler. */ @@ -130,6 +186,37 @@ export interface CoreCourseOptionsHandlerToDisplay { prefetch?(course: any): Promise; } +/** + * Additional data returned if it is a menu item. + */ +export interface CoreCourseOptionsMenuHandlerToDisplay { + /** + * Data to display. + * @type {CoreCourseOptionsMenuHandlerData} + */ + data: CoreCourseOptionsMenuHandlerData; + + /** + * Name of the handler, or name and sub context (AddonMessages, AddonMessages:blockContact, ...). + * @type {string} + */ + name: string; + + /** + * The highest priority is displayed first. + * @type {number} + */ + priority?: number; + + /** + * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline. + * + * @param {any} course The course. + * @return {Promise} Promise resolved when done. + */ + prefetch?(course: any): Promise; +} + /** * Service to interact with plugins to be shown in each course (participants, learning plans, ...). */ @@ -139,7 +226,8 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { protected lastUpdateHandlersForCoursesStart: any = {}; protected coursesHandlers: { [courseId: number]: { - access?: any, navOptions?: any, admOptions?: any, deferred?: PromiseDefer, enabledHandlers?: CoreCourseOptionsHandler[] + access?: any, navOptions?: any, admOptions?: any, deferred?: PromiseDefer, + enabledHandlers?: CoreCourseOptionsHandler[], enabledMenuHandlers?: CoreCourseOptionsMenuHandler[] } } = {}; @@ -258,6 +346,43 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { */ getHandlersToDisplay(injector: Injector, course: any, refresh?: boolean, isGuest?: boolean, navOptions?: any, admOptions?: any): Promise { + return > this.getHandlersToDisplayInternal( + false, injector, course, refresh, isGuest, navOptions, admOptions); + } + + /** + * Get the list of menu 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. + * @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 {Promise} Promise resolved with array of handlers. + */ + getMenuHandlersToDisplay(injector: Injector, course: any, refresh?: boolean, isGuest?: boolean, + navOptions?: any, admOptions?: any): Promise { + return > this.getHandlersToDisplayInternal( + true, injector, course, refresh, isGuest, navOptions, admOptions); + } + + /** + * Get the list of menu 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 {boolean} menu If true, gets menu handlers; false, gets tab handlers + * @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. + * @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 {Promise} Promise resolved with array of handlers. + */ + protected getHandlersToDisplayInternal(menu: boolean, injector: Injector, course: any, refresh: boolean, isGuest: boolean, + navOptions: any, admOptions: any): Promise { course.id = parseInt(course.id, 10); const accessData = { @@ -278,8 +403,16 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { }).then(() => { const promises = []; - this.coursesHandlers[course.id].enabledHandlers.forEach((handler) => { - promises.push(Promise.resolve(handler.getDisplayData(injector, course)).then((data) => { + let handlerList; + if (menu) { + handlerList = this.coursesHandlers[course.id].enabledMenuHandlers; + } else { + handlerList = this.coursesHandlers[course.id].enabledHandlers; + } + + handlerList.forEach((handler) => { + const getFunction = menu ? handler.getMenuDisplayData : handler.getDisplayData; + promises.push(Promise.resolve(getFunction.call(handler, injector, course)).then((data) => { handlersToDisplay.push({ data: data, priority: handler.priority, @@ -444,6 +577,7 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { updateHandlersForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): Promise { const promises = [], enabledForCourse = [], + enabledForCourseMenu = [], siteId = this.sitesProvider.getCurrentSiteId(), now = Date.now(); @@ -456,7 +590,11 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { promises.push(Promise.resolve(handler.isEnabledForCourse(courseId, accessData, navOptions, admOptions)) .then((enabled) => { if (enabled) { - enabledForCourse.push(handler); + if (handler.isMenuHandler) { + enabledForCourseMenu.push( handler); + } else { + enabledForCourse.push(handler); + } } else { return Promise.reject(null); } @@ -476,6 +614,7 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { if (this.isLastUpdateCourseCall(courseId, now) && this.sitesProvider.getCurrentSiteId() === siteId) { // Update the coursesHandlers array with the new enabled addons. this.coursesHandlers[courseId].enabledHandlers = enabledForCourse; + this.coursesHandlers[courseId].enabledMenuHandlers = enabledForCourseMenu; this.loaded[courseId] = true; // Resolve the promise.