Skip to content

Commit

Permalink
MOBILE-4636 course: Expand and collapse sections
Browse files Browse the repository at this point in the history
  • Loading branch information
crazyserver committed Sep 17, 2024
1 parent 074246f commit f0ef97b
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 49 deletions.
7 changes: 7 additions & 0 deletions src/core/components/infinite-loading/infinite-loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
this.action.emit(() => this.complete());
}

/**
* Fire the infinite scroll load more action if needed.
*/
async fireInfiniteScrollIfNeeded(): Promise<void> {
this.checkScrollDistance();
}

/**
* Complete loading.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
<!-- Single section. -->
<div *ngIf="selectedSection && selectedSection.id !== allSectionsId" class="single-section list-item-limited-width">
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}" />
<ion-accordion-group [readonly]="true" value="single">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection, sectionId: 'single'}" />
</ion-accordion-group>
<core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-table-cells-large"
[message]="'core.course.nocontentavailable' | translate" />
</core-dynamic-component>
Expand All @@ -19,11 +21,14 @@
<!-- Multiple sections. -->
<div *ngIf="selectedSection && selectedSection.id === allSectionsId" class="multiple-sections list-item-limited-width">
<core-dynamic-component [component]="allSectionsComponent" [data]="data">
<ng-container *ngFor="let section of sections; index as i">
<ng-container *ngIf="i <= lastShownSectionIndex">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}" />
</ng-container>
</ng-container>
<ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)" [value]="accordionMultipleValue"
#accordionMultiple>
@for (section of sections; track section.id) {
@if ($index <= lastShownSectionIndex) {
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section, sectionId: section.id}" />
}
}
</ion-accordion-group>
</core-dynamic-component>

<core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)" />
Expand Down Expand Up @@ -62,12 +67,12 @@
</ion-fab>

<!-- Template to render a section. -->
<ng-template #sectionTemplate let-section="section">
<section *ngIf="!section.hiddenbynumsections && section.id !== allSectionsId && section.id !== stealthModulesSectionId"
<ng-template #sectionTemplate let-section="section" let-sectionId="sectionId">
<ion-accordion *ngIf="!section.hiddenbynumsections && section.id !== allSectionsId && section.id !== stealthModulesSectionId"
class="core-course-module-list-wrapper" [id]="section.id"
[attr.aria-labelledby]="section.name ? 'core-section-name-' + section.id : null">
<ion-item-divider class="course-section ion-text-wrap" [class.item-dimmed]="section.visible === 0 || section.uservisible === false">
<ion-label>
[attr.aria-labelledby]="section.name ? 'core-section-name-' + section.id : null" [value]="''+sectionId" toggleIconSlot="start">
<ion-item class="course-section divider" [class.item-dimmed]="section.visible === 0 || section.uservisible === false" slot="header">
<ion-label class="ion-text-wrap">
<h2 *ngIf="section.name" class="big" [id]="'core-section-name-' + section.id">
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course.id" />
</h2>
Expand All @@ -91,19 +96,24 @@ <h2 *ngIf="section.name" class="big" [id]="'core-section-name-' + section.id">
</div>
</ion-label>
<ion-badge *ngIf="section.highlighted && highlighted" slot="end">{{highlighted}}</ion-badge>
</ion-item-divider>

<ion-item class="ion-text-wrap section-summary" *ngIf="section.summary">
<ion-label>
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="course.id" />
</ion-label>
</ion-item>

<ng-container *ngFor="let module of section.modules">
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
[showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"
[isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === module.id" [class.core-course-module-not-viewed]="
!viewedModules[module.id] && (!module.completiondata || module.completiondata.state === completionStatusIncomplete)" />
</ng-container>
</section>
<div slot="content">
<ng-container *ngIf="section.expanded">
<ion-item class="ion-text-wrap section-summary" *ngIf="section.summary">
<ion-label>
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="course.id" />
</ion-label>
</ion-item>

<ng-container *ngFor="let module of section.modules">
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
[showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"
[isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === module.id"
[class.core-course-module-not-viewed]="
!viewedModules[module.id] && (!module.completiondata || module.completiondata.state === completionStatusIncomplete)" />
</ng-container>
</ng-container>
</div>
</ion-accordion>
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@
--ion-card-background: transparent;
}

ion-item-divider {
ion-item.divider.course-section {
--background: transparent;
}
}

.single-section ::ng-deep {
ion-item.divider.course-section {
ion-icon.ion-accordion-toggle-icon {
display: none;
}
}
}
136 changes: 112 additions & 24 deletions src/core/features/course/components/course-format/course-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -96,6 +99,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent<any>>;

@ViewChild(CoreInfiniteLoadingComponent) infiteLoading?: CoreInfiniteLoadingComponent;

accordionMultipleValue: string[] = [];

// All the possible component classes.
courseFormatComponent?: Type<unknown>;
singleSectionComponent?: Type<unknown>;
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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<void> {
this.setInputData();
Expand Down Expand Up @@ -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<void> {
protected async treatSections(sections: CoreCourseSectionToDisplay[]): Promise<void> {
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.
Expand Down Expand Up @@ -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<void> {
if (this.viewedModulesInitialized) {
Expand All @@ -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);
}
}
}

/**
Expand Down Expand Up @@ -426,7 +439,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {

// Check current scrolled section.
const allSectionElements: NodeListOf<HTMLElement> =
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;
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -712,28 +731,97 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
*
* @param show Whether if all sections is preferred.
*/
async setAllSectionsPreferred(show: boolean): Promise<void> {
const site = CoreSites.getCurrentSite();

await site?.setLocalSiteConfig(`${COURSE_ALL_SECTIONS_PREFERRED_PREFIX}${this.course.id}`, show ? 1 : 0);
protected async setAllSectionsPreferred(show: boolean): Promise<void> {
await this.currentSite?.setLocalSiteConfig(`${COURSE_ALL_SECTIONS_PREFERRED_PREFIX}${this.course.id}`, show ? 1 : 0);
}

/**
* Check if all sections is preferred for the course.
*
* @returns Whether if all sections is preferred.
*/
async isAllSectionsPreferred(): Promise<boolean> {
const site = CoreSites.getCurrentSite();

protected async isAllSectionsPreferred(): Promise<boolean> {
const showAllSections =
await site?.getLocalSiteConfig<number>(`${COURSE_ALL_SECTIONS_PREFERRED_PREFIX}${this.course.id}`, 0);
await this.currentSite?.getLocalSiteConfig<number>(`${COURSE_ALL_SECTIONS_PREFERRED_PREFIX}${this.course.id}`, 0);

return !!showAllSections;
}

/**
* Save expanded sections for the course.
*/
protected async saveExpandedSections(): Promise<void> {
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<void> {
const expandedSections = await CoreUtils.ignoreErrors(
this.currentSite?.getLocalSiteConfig<string>(`${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.
};
1 change: 1 addition & 0 deletions src/core/features/course/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-';
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit f0ef97b

Please sign in to comment.