2
0
Fork 0

MOBILE-3915 course: Align icons on course index

main
Pau Ferrer Ocaña 2022-02-01 11:40:31 +01:00
parent 6b3d39068c
commit 1e2eb7e4fb
5 changed files with 131 additions and 107 deletions

View File

@ -12,9 +12,9 @@
</ion-header> </ion-header>
<ion-content> <ion-content>
<ion-list id="core-course-section-selector" role="listbox" aria-labelledby="core-course-section-selector-label"> <ion-list id="core-course-section-selector" role="listbox" aria-labelledby="core-course-section-selector-label">
<ng-container *ngFor="let section of sections"> <ng-container *ngFor="let section of sectionsToRender">
<ion-item *ngIf="allSectionId == section.id" class="ion-text-wrap divider" (click)="selectSection($event, section)" button <ion-item *ngIf="allSectionId == section.id" class="ion-text-wrap divider core-course-index-all"
[class.item-current]="selectedId === section.id" detail="false"> (click)="selectSectionOrModule($event, section.id)" button [class.item-current]="selectedId === section.id" detail="false">
<ion-label> <ion-label>
<p class="item-heading"> <p class="item-heading">
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">
@ -22,16 +22,17 @@
</p> </p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ng-container *ngIf="allSectionId != section.id && !section.hiddenbynumsections && <ng-container *ngIf="allSectionId != section.id">
section.id != stealthModulesSectionId && section.uservisible !== false"> <ion-item class="ion-text-wrap divider section" (click)="selectSectionOrModule($event, section.id)" button
<ion-item class="ion-text-wrap divider section" (click)="selectSection($event, section)" [class.item-current]="selectedId === section.id" [class.item-dimmed]="section.visible === 0" detail="false"
[button]="section.visible !== 0 && section.uservisible !== false" [class.item-current]="selectedId === section.id" sticky="true">
[class.item-dimmed]="section.visible === 0" detail="false" sticky="true"> <ion-icon *ngIf="section.hasVisibleModules" [name]="section.expanded ? 'fas-chevron-down' : 'fas-chevron-right'"
<ion-icon [name]="section.expanded ? 'fas-chevron-down' : 'fas-chevron-right'" flip-rtl slot="start" flip-rtl slot="start" class="expandable-status-icon" (click)="toggleExpand($event, section)"
class="expandable-status-icon" (click)="toggleExpand($event, section)"
[attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate" [attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate"
[attr.aria-expanded]="section.expanded" [attr.aria-controls]="'core-course-index-section-' + section.id"> [attr.aria-expanded]="section.expanded" [attr.aria-controls]="'core-course-index-section-' + section.id">
</ion-icon> </ion-icon>
<ion-icon *ngIf="!section.hasVisibleModules" name="" slot="start" aria-hidden="true" class="expandable-status-icon">
</ion-icon>
<ion-label> <ion-label>
<p class="item-heading"> <p class="item-heading">
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">
@ -42,16 +43,14 @@
<ion-icon name="fas-lock" *ngIf="section.availabilityinfo" slot="end" class="restricted" <ion-icon name="fas-lock" *ngIf="section.availabilityinfo" slot="end" class="restricted"
[attr.aria-label]="'core.restricted' | translate"></ion-icon> [attr.aria-label]="'core.restricted' | translate"></ion-icon>
</ion-item> </ion-item>
<div [hidden]="!section.expanded" [id]="'core-course-index-section-' + section.id">
<ng-container *ngIf="section.expanded"> <ng-container *ngIf="section.expanded">
<ng-container *ngFor="let module of section.modules"> <ng-container *ngFor="let module of section.modules">
<ion-item [class.item-dimmed]="module.visible === 0" <ion-item [class.item-dimmed]="!module.visible" (click)="selectSectionOrModule($event, section.id, module.id)"
*ngIf="module.visibleoncoursepage !== 0 && !module.noviewlink" button>
(click)="selectModule($event, section, module)" button>
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined" <ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined"
slot="start" aria-hidden="true"></ion-icon> slot="start" aria-hidden="true"></ion-icon>
<ion-icon class="completioninfo completion_incomplete" name="far-circle" <ion-icon class="completioninfo completion_incomplete" name="far-circle" *ngIf="module.completionStatus === 0"
*ngIf="module.completionStatus === 0" slot="start" [attr.aria-label]="'core.course.todo' | translate"> slot="start" [attr.aria-label]="'core.course.todo' | translate">
</ion-icon> </ion-icon>
<ion-icon class="completioninfo completion_complete" name="fas-circle" <ion-icon class="completioninfo completion_complete" name="fas-circle"
*ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start" *ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
@ -63,16 +62,15 @@
<ion-label> <ion-label>
<p class="item-heading"> <p class="item-heading">
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id" <core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="module.courseid"> [courseId]="module.course">
</core-format-text> </core-format-text>
</p> </p>
</ion-label> </ion-label>
<ion-icon name="fas-lock" *ngIf="module.uservisible === false" slot="end" class="restricted" <ion-icon name="fas-lock" *ngIf="!module.uservisible" slot="end" class="restricted"
[attr.aria-label]="'core.restricted' | translate"></ion-icon> [attr.aria-label]="'core.restricted' | translate"></ion-icon>
</ion-item> </ion-item>
</ng-container> </ng-container>
</ng-container> </ng-container>
</div>
</ng-container> </ng-container>
</ng-container> </ng-container>
</ion-list> </ion-list>

View File

@ -16,18 +16,21 @@ ion-icon.completioninfo {
width: 18px; width: 18px;
} }
ion-item.section::part(native) { ion-item::part(native) {
--padding-start: 0; --padding-start: 0;
} }
ion-icon.expandable-status-icon { ion-icon {
margin: 0; margin: 0;
@include padding(12px, 32px, 12px, 16px); @include padding(12px, 32px, 12px, 16px);
} }
ion-item.core-course-index-all::part(native) {
--padding-start: 16px;
}
ion-item.item-current ion-icon.expandable-status-icon { ion-item.item-current ion-icon.expandable-status-icon {
@include padding(null, null, null, 11px); @include padding(null, null, null, 11px);
} }
ion-icon.restricted { ion-icon.restricted {

View File

@ -13,19 +13,18 @@
// limitations under the License. // limitations under the License.
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
import { import {
CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionStatus,
CoreCourseModuleCompletionTracking, CoreCourseModuleCompletionTracking,
CoreCourseProvider, CoreCourseProvider,
} from '@features/course/services/course'; } from '@features/course/services/course';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreCourseSection } from '@features/course/services/course-helper';
import { CoreUtils } from '@services/utils/utils';
import { ModalController } from '@singletons';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { IonContent } from '@ionic/angular'; import { IonContent } from '@ionic/angular';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { ModalController } from '@singletons';
/** /**
* Component to display course index modal. * Component to display course index modal.
@ -39,13 +38,13 @@ export class CoreCourseCourseIndexComponent implements OnInit {
@ViewChild(IonContent) content?: IonContent; @ViewChild(IonContent) content?: IonContent;
@Input() sections?: CourseIndexSection[]; @Input() sections: CoreCourseSection[] = [];
@Input() selectedId?: number; @Input() selectedId?: number;
@Input() course?: CoreCourseAnyCourseData; @Input() course?: CoreCourseAnyCourseData;
stealthModulesSectionId = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
allSectionId = CoreCourseProvider.ALL_SECTIONS_ID; allSectionId = CoreCourseProvider.ALL_SECTIONS_ID;
highlighted?: string; highlighted?: string;
sectionsToRender: CourseIndexSection[] = [];
constructor( constructor(
protected elementRef: ElementRef, protected elementRef: ElementRef,
@ -70,28 +69,46 @@ export class CoreCourseCourseIndexComponent implements OnInit {
return; return;
} }
// Collapse all sections first.
this.sections.forEach((section) => section.expanded = false);
const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections); const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
currentSection.highlighted = true;
if (this.selectedId === undefined) { if (this.selectedId === undefined) {
currentSection.expanded = true; // Highlight current section if none is selected.
this.selectedId = currentSection.id; this.selectedId = currentSection.id;
} else {
const selectedSection = this.sections.find((section) => section.id == this.selectedId);
if (selectedSection) {
selectedSection.expanded = true;
}
} }
this.sections.forEach((section) => { // Clone sections to add information.
section.modules.forEach((module) => { this.sectionsToRender = this.sections
module.completionStatus = module.completiondata === undefined || .filter((section) => !section.hiddenbynumsections &&
section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID &&
section.uservisible !== false)
.map((section) => {
const modules = section.modules
.filter((module) => module.visibleoncoursepage !== 0 && !module.noviewlink)
.map((module) => {
const completionStatus = module.completiondata === undefined ||
module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE
? undefined ? undefined
: module.completiondata.state; : module.completiondata.state;
return {
id: module.id,
name: module.name,
course: module.course,
visible: !!module.visible,
uservisible: !!module.uservisible,
completionStatus,
};
}); });
return {
id: section.id,
name: section.name,
availabilityinfo: !!section.availabilityinfo,
expanded: section.id === this.selectedId,
highlighted: currentSection?.id === section.id,
hasVisibleModules: modules.length > 0,
modules: modules,
};
}); });
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course); this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
@ -102,7 +119,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
this.content, this.content,
'.item.item-current', '.item.item-current',
); );
}, 200); }, 300);
} }
/** /**
@ -128,39 +145,33 @@ export class CoreCourseCourseIndexComponent implements OnInit {
* Select a section. * Select a section.
* *
* @param event Event. * @param event Event.
* @param section Selected section object. * @param sectionId Selected section id.
* @param moduleId Selected module id, if any.
*/ */
selectSection(event: Event, section: CoreCourseSection): void { selectSectionOrModule(event: Event, sectionId: number, moduleId?: number): void {
if (section.uservisible !== false) { ModalController.dismiss({ event, sectionId, moduleId });
ModalController.dismiss({ event, section });
}
}
/**
* Select a section and open a module
*
* @param event Event.
* @param section Selected section object.
* @param module Selected module object.
*/
selectModule(event: Event,section: CoreCourseSection, module: CoreCourseModuleData): void {
if (module.uservisible !== false) {
ModalController.dismiss({ event, section, module });
}
} }
} }
type CourseIndexSection = Omit<CoreCourseSection, 'modules'> & { type CourseIndexSection = {
highlighted?: boolean; id: number;
expanded?: boolean; name: string;
modules: (CoreCourseModuleData & { highlighted: boolean;
expanded: boolean;
hasVisibleModules: boolean;
availabilityinfo: boolean;
modules: {
id: number;
course: number;
visible: boolean;
uservisible: boolean;
completionStatus?: CoreCourseModuleCompletionStatus; completionStatus?: CoreCourseModuleCompletionStatus;
})[]; }[];
}; };
export type CoreCourseIndexSectionWithModule = { export type CoreCourseIndexSectionWithModule = {
event: Event; event: Event;
section: CourseIndexSection; sectionId: number;
module?: CoreCourseModuleData; moduleId?: number;
}; };

View File

@ -69,7 +69,6 @@
class="section-wrapper" [id]="section.id"> class="section-wrapper" [id]="section.id">
<ion-item-divider class="course-section ion-text-wrap" color="light" <ion-item-divider class="course-section ion-text-wrap" color="light"
[class.item-dimmed]="section.visible === 0 || section.uservisible === false"> [class.item-dimmed]="section.visible === 0 || section.uservisible === false">
<ion-icon name="fas-folder" aria-label="hidden" slot="start"></ion-icon>
<ion-label> <ion-label>
<h2 *ngIf="section.name"> <h2 *ngIf="section.name">
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course.id"> <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course.id">

View File

@ -312,20 +312,33 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}, },
}); });
if (data) { if (!data) {
this.sectionChanged(data.section); return;
if (data.module) { }
if (!data.module.handlerData) { const section = this.sections.find((section) => section.id == data.sectionId);
data.module.handlerData = if (!section) {
await CoreCourseModuleDelegate.getModuleDataFor(data.module.modname, data.module, this.course.id); return;
}
this.sectionChanged(section);
if (!data.moduleId) {
return;
}
const module = section.modules.find((module) => module.id == data.moduleId);
if (!module) {
return;
} }
if (data.module.uservisible !== false && data.module.handlerData?.action) { if (!module.handlerData) {
data.module.handlerData.action(data.event, data.module, data.module.course); module.handlerData =
} await CoreCourseModuleDelegate.getModuleDataFor(module.modname, module, this.course.id);
this.moduleId = data.module.id;
} }
if (module.uservisible !== false && module.handlerData?.action) {
module.handlerData.action(data.event, module, module.course);
} }
this.moduleId = data.moduleId;
} }
/** /**