Merge pull request #3087 from crazyserver/MOBILE-3915

MOBILE-3915 course: Align icons on course index
main
Dani Palou 2022-02-02 11:55:39 +01:00 committed by GitHub
commit b2eb5e25d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 147 additions and 123 deletions

View File

@ -26,7 +26,7 @@ import { CoreSite } from '@classes/site';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
import { AddonBlockMyOverviewFilterOptionsComponent } from '../filteroptions/filteroptions'; import { AddonBlockMyOverviewFilterOptionsComponent } from '../filteroptions/filteroptions';
import { IonSearchbar } from '@ionic/angular'; import { IonSearchbar } from '@ionic/angular';
import moment from 'moment'; import moment from 'moment';

View File

@ -27,7 +27,7 @@ import {
CoreEnrolledCourseDataWithOptions, CoreEnrolledCourseDataWithOptions,
} from '@features/courses/services/courses-helper'; } from '@features/courses/services/courses-helper';
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';

View File

@ -21,7 +21,7 @@ import {
CoreEnrolledCourseDataWithOptions, CoreEnrolledCourseDataWithOptions,
} from '@features/courses/services/courses-helper'; } from '@features/courses/services/courses-helper';
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';

View File

@ -25,26 +25,26 @@ function buildRoutes(injector: Injector): Routes {
data: { data: {
mainMenuTabRoot: AddonCalendarMainMenuHandlerService.PAGE_NAME, mainMenuTabRoot: AddonCalendarMainMenuHandlerService.PAGE_NAME,
}, },
loadChildren: () => import('@/addons/calendar/pages/index/index.module').then(m => m.AddonCalendarIndexPageModule), loadChildren: () => import('@addons/calendar/pages/index/index.module').then(m => m.AddonCalendarIndexPageModule),
}, },
{ {
path: 'settings', path: 'settings',
loadChildren: () => loadChildren: () =>
import('@/addons/calendar/pages/settings/settings.module').then(m => m.AddonCalendarSettingsPageModule), import('@addons/calendar/pages/settings/settings.module').then(m => m.AddonCalendarSettingsPageModule),
}, },
{ {
path: 'day', path: 'day',
loadChildren: () => loadChildren: () =>
import('@/addons/calendar/pages/day/day.module').then(m => m.AddonCalendarDayPageModule), import('@addons/calendar/pages/day/day.module').then(m => m.AddonCalendarDayPageModule),
}, },
{ {
path: 'event/:id', path: 'event/:id',
loadChildren: () => import('@/addons/calendar/pages/event/event.module').then(m => m.AddonCalendarEventPageModule), loadChildren: () => import('@addons/calendar/pages/event/event.module').then(m => m.AddonCalendarEventPageModule),
}, },
{ {
path: 'edit/:eventId', path: 'edit/:eventId',
loadChildren: () => loadChildren: () =>
import('@/addons/calendar/pages/edit-event/edit-event.module').then(m => m.AddonCalendarEditEventPageModule), import('@addons/calendar/pages/edit-event/edit-event.module').then(m => m.AddonCalendarEditEventPageModule),
}, },
...buildTabMainRoutes(injector, { ...buildTabMainRoutes(injector, {
redirectTo: 'index', redirectTo: 'index',

View File

@ -15,7 +15,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { FileEntry } from '@ionic-native/file'; import { FileEntry } from '@ionic-native/file/ngx';
import { CoreFile } from '@services/file'; import { CoreFile } from '@services/file';
import { CoreFileEntry } from '@services/file-helper'; import { CoreFileEntry } from '@services/file-helper';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';

View File

@ -37,7 +37,7 @@ export const ADDON_NOTIFICATIONS_SERVICES: Type<unknown>[] = [
const routes: Routes = [ const routes: Routes = [
{ {
path: AddonNotificationsMainMenuHandlerService.PAGE_NAME, path: AddonNotificationsMainMenuHandlerService.PAGE_NAME,
loadChildren: () => import('@/addons/notifications/notifications-lazy.module').then(m => m.AddonNotificationsLazyModule), loadChildren: () => import('@addons/notifications/notifications-lazy.module').then(m => m.AddonNotificationsLazyModule),
}, },
]; ];
const preferencesRoutes: Routes = [ const preferencesRoutes: Routes = [

View File

@ -28,8 +28,8 @@ import {
AddonPrivateFilesFile, AddonPrivateFilesFile,
AddonPrivateFilesGetUserInfoWSResult, AddonPrivateFilesGetUserInfoWSResult,
AddonPrivateFilesGetFilesWSParams, AddonPrivateFilesGetFilesWSParams,
} from '@/addons/privatefiles/services/privatefiles'; } from '@addons/privatefiles/services/privatefiles';
import { AddonPrivateFilesHelper } from '@/addons/privatefiles/services/privatefiles-helper'; import { AddonPrivateFilesHelper } from '@addons/privatefiles/services/privatefiles-helper';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';

View File

@ -30,7 +30,7 @@ export const ADDON_PRIVATEFILES_SERVICES: Type<unknown>[] = [
const routes: Routes = [ const routes: Routes = [
{ {
path: AddonPrivateFilesUserHandlerService.PAGE_NAME, path: AddonPrivateFilesUserHandlerService.PAGE_NAME,
loadChildren: () => import('@/addons/privatefiles/privatefiles-lazy.module').then(m => m.AddonPrivateFilesLazyModule), loadChildren: () => import('@addons/privatefiles/privatefiles-lazy.module').then(m => m.AddonPrivateFilesLazyModule),
}, },
]; ];

View File

@ -14,7 +14,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AddonPrivateFiles } from '@/addons/privatefiles/services/privatefiles'; import { AddonPrivateFiles } from '@addons/privatefiles/services/privatefiles';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { import {
CoreUserDelegateContext, CoreUserDelegateContext,

View File

@ -24,7 +24,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { CoreModule } from '@/core/core.module'; import { CoreModule } from '@/core/core.module';
import { AddonsModule } from '@/addons/addons.module'; import { AddonsModule } from '@addons/addons.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';

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;
} }
/** /**

View File

@ -24,7 +24,7 @@ import {
} from './courses'; } from './courses';
import { makeSingleton, Translate } from '@singletons'; import { makeSingleton, Translate } from '@singletons';
import { CoreWSExternalFile } from '@services/ws'; import { CoreWSExternalFile } from '@services/ws';
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
/** /**
* Helper to gather some common courses functions. * Helper to gather some common courses functions.