diff --git a/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html b/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html index 6885e5146..79c4f4af5 100644 --- a/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html +++ b/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html @@ -4,7 +4,7 @@ - + this.complete()); } + /** + * Fire the infinite scroll load more action if needed. + */ + async fireInfiniteScrollIfNeeded(): Promise { + this.checkScrollDistance(); + } + /** * Complete loading. */ diff --git a/src/core/features/course/components/course-format/course-format.html b/src/core/features/course/components/course-format/course-format.html index 4651ea3f3..38d99fd84 100644 --- a/src/core/features/course/components/course-format/course-format.html +++ b/src/core/features/course/components/course-format/course-format.html @@ -8,22 +8,27 @@ -
+
- + + +
-
+
- - - - - + + @for (section of sections; track section.id) { + @if ($index <= lastShownSectionIndex) { + + } + } + @@ -62,12 +67,12 @@ - -
+ - - + [attr.aria-labelledby]="section.name ? 'core-section-name-' + section.id : null" [value]="''+sectionId" toggleIconSlot="start"> + +

@@ -91,19 +96,24 @@
{{highlighted}} - - - - - - - - - - +
+ + + + + + + + + + + +
+ diff --git a/src/core/features/course/components/course-format/course-format.scss b/src/core/features/course/components/course-format/course-format.scss index 6b7420c86..8188f07d8 100644 --- a/src/core/features/course/components/course-format/course-format.scss +++ b/src/core/features/course/components/course-format/course-format.scss @@ -14,3 +14,26 @@ .course-section { --inner-padding-end: 12px; } + +.multiple-sections .core-course-module-list-wrapper { + border: var(--ion-card-border-width) solid var(--ion-card-border-color); + border-radius: var(--ion-card-radius); + margin: 8px 4px; + width: calc(100% - 8px); + + ion-card { + --ion-card-background: transparent; + } + + ion-item.divider.course-section { + --background: transparent; + } +} + +.single-section ::ng-deep { + ion-item.divider.course-section { + ion-icon.ion-accordion-toggle-icon { + display: none; + } + } +} diff --git a/src/core/features/course/components/course-format/course-format.ts b/src/core/features/course/components/course-format/course-format.ts index 6ff2a5804..4308a4616 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -24,6 +24,7 @@ import { Type, ElementRef, ChangeDetectorRef, + ViewChild, } from '@angular/core'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; @@ -39,7 +40,7 @@ import { } from '@features/course/services/course-helper'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { IonContent } from '@ionic/angular'; +import { AccordionGroupChangeEventDetail, IonContent } from '@ionic/angular'; import { CoreUtils } from '@services/utils/utils'; import { CoreCourseIndexSectionWithModule } from '../course-index/course-index'; import { CoreBlockHelper } from '@features/block/services/block-helper'; @@ -57,8 +58,10 @@ import { CoreSharedModule } from '@/core/shared.module'; import { CoreBlockComponentsModule } from '@features/block/components/components.module'; import { CoreCourseComponentsModule } from '../components.module'; import { CoreSites } from '@services/sites'; -import { COURSE_ALL_SECTIONS_PREFERRED_PREFIX } from '@features/course/constants'; +import { COURSE_ALL_SECTIONS_PREFERRED_PREFIX, COURSE_EXPANDED_SECTIONS_PREFIX } from '@features/course/constants'; import { toBoolean } from '@/core/transforms/boolean'; +import { CoreInfiniteLoadingComponent } from '@components/infinite-loading/infinite-loading'; +import { CoreSite } from '@classes/sites/site'; /** * Component to display course contents using a certain format. If the format isn't found, use default one. @@ -96,6 +99,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-explicit-any @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList>; + @ViewChild(CoreInfiniteLoadingComponent) infiteLoading?: CoreInfiniteLoadingComponent; + + accordionMultipleValue: string[] = []; + // All the possible component classes. courseFormatComponent?: Type; singleSectionComponent?: Type; @@ -119,9 +126,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { displayCourseIndex = false; displayBlocks = false; hasBlocks = false; - selectedSection?: CoreCourseSection; - previousSection?: CoreCourseSection; - nextSection?: CoreCourseSection; + selectedSection?: CoreCourseSectionToDisplay; + previousSection?: CoreCourseSectionToDisplay; + nextSection?: CoreCourseSectionToDisplay; allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID; stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; loaded = false; @@ -136,6 +143,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { protected modViewedObserver?: CoreEventObserver; protected lastCourseFormat?: string; protected viewedModulesInitialized = false; + protected currentSite?: CoreSite; constructor( protected content: IonContent, @@ -158,6 +166,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { return; } + this.currentSite = CoreSites.getRequiredCurrentSite(); + // Listen for select course tab events to select the right section if needed. this.selectTabObserver = CoreEvents.on(CoreEvents.SELECT_COURSE_TAB, (data) => { if (data.name) { @@ -196,10 +206,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } this.changeDetectorRef.markForCheck(); }); + + this.initializeExpandedSections(); } /** - * Detect changes on input properties. + * @inheritdoc */ async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise { this.setInputData(); @@ -287,14 +299,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * Treat received sections. * * @param sections Sections to treat. - * @returns Promise resolved when done. */ - protected async treatSections(sections: CoreCourseSection[]): Promise { + protected async treatSections(sections: CoreCourseSectionToDisplay[]): Promise { const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID; const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections); await this.initializeViewedModules(); - if (this.selectedSection) { const selectedSection = this.selectedSection; // We have a selected section, but the list has changed. Search the section in the list. @@ -366,14 +376,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.loaded = true; this.sectionChanged(section, moduleId); } - - return; } /** * Initialize viewed modules. - * - * @returns Promise resolved when done. */ protected async initializeViewedModules(): Promise { if (this.viewedModulesInitialized) { @@ -387,6 +393,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { viewedModules.forEach(entry => { this.viewedModules[entry.cmId] = true; }); + + if (this.lastModuleViewed) { + const section = this.getViewedModuleSection(this.sections, this.lastModuleViewed); + if (section) { + this.setSectionExpanded(section); + } + } } /** @@ -426,7 +439,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { // Check current scrolled section. const allSectionElements: NodeListOf = - this.elementRef.nativeElement.querySelectorAll('section.core-course-module-list-wrapper'); + this.elementRef.nativeElement.querySelectorAll('.core-course-module-list-wrapper'); const scroll = await this.content.getScrollElement(); const containerTop = scroll.getBoundingClientRect().top; @@ -515,12 +528,15 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * @param newSection The new selected section. * @param moduleId The module to scroll to. */ - sectionChanged(newSection: CoreCourseSection, moduleId?: number): void { + sectionChanged(newSection: CoreCourseSectionToDisplay, moduleId?: number): void { const previousValue = this.selectedSection; this.selectedSection = newSection; + this.data.section = this.selectedSection; if (newSection.id !== this.allSectionsId) { + this.setSectionExpanded(newSection); + // Select next and previous sections to show the arrows. const i = this.sections.findIndex((value) => this.compareSections(value, newSection)); @@ -627,7 +643,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { modulesLoaded < CoreCourseFormatComponent.LOAD_MORE_ACTIVITIES) { this.lastShownSectionIndex++; - if (!this.sections[this.lastShownSectionIndex].hasContent || !this.sections[this.lastShownSectionIndex].modules) { + // Skip sections without content, with stealth modules or collapsed. + if (!this.sections[this.lastShownSectionIndex].hasContent || + !this.sections[this.lastShownSectionIndex].modules || + !this.sections[this.lastShownSectionIndex].expanded) { continue; } @@ -712,10 +731,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * * @param show Whether if all sections is preferred. */ - async setAllSectionsPreferred(show: boolean): Promise { - const site = CoreSites.getCurrentSite(); - - await site?.setLocalSiteConfig(`${COURSE_ALL_SECTIONS_PREFERRED_PREFIX}${this.course.id}`, show ? 1 : 0); + protected async setAllSectionsPreferred(show: boolean): Promise { + await this.currentSite?.setLocalSiteConfig(`${COURSE_ALL_SECTIONS_PREFERRED_PREFIX}${this.course.id}`, show ? 1 : 0); } /** @@ -723,17 +740,88 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * * @returns Whether if all sections is preferred. */ - async isAllSectionsPreferred(): Promise { - const site = CoreSites.getCurrentSite(); - + protected async isAllSectionsPreferred(): Promise { const showAllSections = - await site?.getLocalSiteConfig(`${COURSE_ALL_SECTIONS_PREFERRED_PREFIX}${this.course.id}`, 0); + await this.currentSite?.getLocalSiteConfig(`${COURSE_ALL_SECTIONS_PREFERRED_PREFIX}${this.course.id}`, 0); return !!showAllSections; } + /** + * Save expanded sections for the course. + */ + protected async saveExpandedSections(): Promise { + const expandedSections = this.sections.filter((section) => section.expanded).map((section) => section.id).join(','); + + await this.currentSite?.setLocalSiteConfig(`${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`, expandedSections); + } + + /** + * Initializes the expanded sections for the course. + */ + protected async initializeExpandedSections(): Promise { + const expandedSections = await CoreUtils.ignoreErrors( + this.currentSite?.getLocalSiteConfig(`${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`), + ); + + // Expand all sections if not defined. + if (expandedSections === undefined) { + this.sections.forEach((section) => { + section.expanded = true; + this.accordionMultipleValue.push(section.id.toString()); + }); + + return; + } + + this.accordionMultipleValue = expandedSections.split(','); + + this.sections.forEach((section) => { + section.expanded = this.accordionMultipleValue.includes(section.id.toString()); + }); + } + + /** + * Toogle the visibility of a section (expand/collapse). + * + * @param ev The event of the accordion. + */ + accordionMultipleChange(ev: AccordionGroupChangeEventDetail): void { + const sectionIds = ev.value as string[] | undefined; + this.sections.forEach((section) => { + section.expanded = false; + }); + + sectionIds?.forEach((sectionId) => { + const sId = Number(sectionId); + const section = this.sections.find((section) => section.id === sId); + if (section) { + section.expanded = true; + } + }); + + // Save course expanded sections. + this.saveExpandedSections(); + + this.infiteLoading?.fireInfiniteScrollIfNeeded(); + } + + /** + * Expands a section and save state. + * + * @param section The section to expand. + */ + protected setSectionExpanded(section: CoreCourseSectionToDisplay): void { + section.expanded = true; + if (!this.accordionMultipleValue.includes(section.id.toString())) { + this.accordionMultipleValue.push(section.id.toString()); + this.saveExpandedSections(); + } + } + } type CoreCourseSectionToDisplay = CoreCourseSection & { highlighted?: boolean; + expanded?: boolean; // The aim of this property is to avoid DOM overloading. }; diff --git a/src/core/features/course/components/module/core-course-module.html b/src/core/features/course/components/module/core-course-module.html index 1a296ac1b..f11390d14 100644 --- a/src/core/features/course/components/module/core-course-module.html +++ b/src/core/features/course/components/module/core-course-module.html @@ -1,7 +1,7 @@ diff --git a/src/core/features/course/components/module/module.scss b/src/core/features/course/components/module/module.scss index 19f40ee3c..69736bbd9 100644 --- a/src/core/features/course/components/module/module.scss +++ b/src/core/features/course/components/module/module.scss @@ -4,13 +4,15 @@ --horizontal-margin: 12px; --vertical-margin: 12px; --card-padding: 16px; + --card-border-width: 0px; + --card-radius: 0px; + --card-background: transparent; ion-card { margin: var(--vertical-margin) var(--horizontal-margin); - - &.activityinline { - border: 0px; - } + --ion-card-border-width: var(--card-border-width); + --ion-card-radius: var(--card-radius); + --ion-card-background: var(--card-background); } ion-item { @@ -125,6 +127,7 @@ } .activity-extrabadges { + font: var(--mdl-typography-body-font-md); color: var(--medium); } @@ -182,13 +185,11 @@ } &.indented ion-card { - border: none; - --ion-card-radius: 0; @include margin-horizontal(calc(var(--horizontal-margin) + 1rem), null); } - &.indented + ::ng-deep core-course-module.indented ion-card { - border-top: 1px solid var(--border-color); + & + ::ng-deep core-course-module ion-card { + border-top: 1px solid var(--ion-card-border-color); } // Hide download folder icon meanwhile MOBILE-4147 is not solved diff --git a/src/core/features/course/components/module/module.ts b/src/core/features/course/components/module/module.ts index afe4c1545..4f1063fb5 100644 --- a/src/core/features/course/components/module/module.ts +++ b/src/core/features/course/components/module/module.ts @@ -66,7 +66,6 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { prefetchStatusIcon$ = new BehaviorSubject(''); // Module prefetch status icon. prefetchStatusText$ = new BehaviorSubject(''); // Module prefetch status text. moduleHasView = true; - activityInline = false; protected prefetchHandler?: CoreCourseModulePrefetchHandler; @@ -103,18 +102,6 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title; this.moduleHasView = CoreCourse.moduleHasView(this.module); - if ( - this.module.handlerData.hasCustomCmListItem && - (!this.showAvailability || !this.module.availabilityinfo) && - (!this.showCompletion || !this.hasCompletion) && - (!this.showActivityDates || !this.module.dates?.length) && - !this.module.groupmode && - !(this.module.visible === 0) && - !(this.module.visible !== 0 && this.module.isStealth) - ) { - this.activityInline = true; - } - if (this.showDownloadStatus && this.module.handlerData.showDownloadButton) { const status = await CoreCourseModulePrefetchDelegate.getDownloadedModuleStatus(this.module, this.module.course); this.updateModuleStatus(status); diff --git a/src/core/features/course/constants.ts b/src/core/features/course/constants.ts index df774a9a8..aecd60ad6 100644 --- a/src/core/features/course/constants.ts +++ b/src/core/features/course/constants.ts @@ -18,3 +18,4 @@ export const CONTENTS_PAGE_NAME = 'contents'; export const COURSE_CONTENTS_PATH = `${COURSE_PAGE_NAME}/${COURSE_INDEX_PATH}/${CONTENTS_PAGE_NAME}`; export const COURSE_ALL_SECTIONS_PREFERRED_PREFIX = 'CoreCourseFormatAllSectionsPreferred-'; +export const COURSE_EXPANDED_SECTIONS_PREFIX = 'CoreCourseFormatExpandedSections-'; diff --git a/src/core/features/course/pages/list-mod-type/list-mod-type.html b/src/core/features/course/pages/list-mod-type/list-mod-type.html index ce6264820..b7358a4d1 100644 --- a/src/core/features/course/pages/list-mod-type/list-mod-type.html +++ b/src/core/features/course/pages/list-mod-type/list-mod-type.html @@ -8,7 +8,7 @@ - + @@ -18,7 +18,7 @@ - +

@@ -30,7 +30,7 @@ - + diff --git a/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_53.png b/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_53.png index d1cd672aa..18fa3d122 100644 Binary files a/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_53.png and b/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_53.png differ diff --git a/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_57.png b/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_57.png index a58892cff..22518cc73 100644 Binary files a/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_57.png and b/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_57.png differ diff --git a/src/core/features/sitehome/pages/index/index.html b/src/core/features/sitehome/pages/index/index.html index 6190b15c3..168648f1c 100644 --- a/src/core/features/sitehome/pages/index/index.html +++ b/src/core/features/sitehome/pages/index/index.html @@ -8,9 +8,37 @@ - + - +
+ + +

+ +

+
+ + {{ 'core.course.hiddenfromstudents' | translate }} + +
+
+ + {{ 'core.notavailable' | translate }} + +
+
+ + + + + + +
+
+
+ @@ -18,11 +46,10 @@ - +
- diff --git a/src/core/features/sitehome/pages/index/index.scss b/src/core/features/sitehome/pages/index/index.scss index 562b7a536..f28b3f94d 100644 --- a/src/core/features/sitehome/pages/index/index.scss +++ b/src/core/features/sitehome/pages/index/index.scss @@ -10,6 +10,21 @@ ion-item ion-icon { @include margin-horizontal(null, var(--margin-end)); } -core-spacer { - --spacer-horizontal: 10px; +section.core-course-module-list-wrapper { + border: var(--ion-card-border-width) solid var(--ion-card-border-color); + border-radius: var(--ion-card-radius); + margin: 12px; + + ion-card { + --ion-card-background: transparent; + } + + ion-item-divider { + --background: transparent; + } +} + +core-course-module.core-sitehome-news { + --card-border-width: var(--ion-card-border-width); + --card-radius: var(--ion-card-radius); } diff --git a/src/testing/services/behat-blocking.ts b/src/testing/services/behat-blocking.ts index e82a66509..ed6a2c646 100644 --- a/src/testing/services/behat-blocking.ts +++ b/src/testing/services/behat-blocking.ts @@ -24,7 +24,7 @@ import { NavigationError, NavigationStart, } from '@angular/router'; -import { Subscription, filter } from 'rxjs'; +import { filter } from 'rxjs'; import { CoreNavigator } from '@services/navigator'; /** @@ -39,7 +39,6 @@ export class TestingBehatBlockingService { protected lastMutation = 0; protected initialized = false; protected keyIndex = 0; - protected navSubscription?: Subscription; /** * Listen to mutations and override XML Requests. @@ -60,7 +59,7 @@ export class TestingBehatBlockingService { win.M.util = win.M.util ?? {}; win.M.util.pending_js = win.M.util.pending_js ?? []; - this.navSubscription = Router.events + Router.events .pipe(filter(event => event instanceof NavigationStart || event instanceof NavigationEnd || diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 2c4350d3e..74b62ffbd 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -207,9 +207,7 @@ img[core-external-content]:not([src]) { border-radius: var(--mdl-shape-borderRadius-lg); } -ion-list.core-course-module-list-wrapper, .list-item-limited-width, -.core-course-module-list-wrapper, ion-content.limited-width > :not([slot]) { max-width: var(--list-item-max-width); margin-left: auto !important;