MOBILE-3833 course: Collapse sections in downloads page
parent
37af8e3c69
commit
0298273fc4
|
@ -47,7 +47,13 @@
|
|||
<ng-container *ngFor="let section of sections">
|
||||
<ion-card class="section" *ngIf="section.modules.length > 0">
|
||||
<ion-card-header>
|
||||
<ion-item class="ion-no-padding" lines="full">
|
||||
<ion-item class="ion-no-padding" [lines]="section.expanded ? 'full' : 'none'" button detail="false"
|
||||
(click)="toggleExpand($event, section)" [class.core-course-storage-section-expanded]="section.expanded"
|
||||
[attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate"
|
||||
[attr.aria-expanded]="section.expanded" [attr.aria-controls]="'core-course-storage-section-' + section.id">
|
||||
<ion-icon name="fas-chevron-right" flip-rtl slot="start" class="expandable-status-icon"
|
||||
[class.expandable-status-icon-expanded]="section.expanded">
|
||||
</ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading ion-text-wrap">
|
||||
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="section.course"
|
||||
|
@ -93,44 +99,46 @@
|
|||
</div>
|
||||
</ion-item>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<ng-container *ngFor="let module of section.modules">
|
||||
<ion-item class="ion-no-padding core-course-storage-activity"
|
||||
*ngIf="downloadEnabled || (!module.calculatingSize && module.totalSize > 0)">
|
||||
<core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
|
||||
[modname]="module.modname" [componentId]="module.instance">
|
||||
</core-mod-icon>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h3 class="{{module.handlerData!.class}} addon-storagemanager-module-size">
|
||||
<core-format-text [text]="module.handlerData.title" [courseId]="module.course" contextLevel="module"
|
||||
[contextInstanceId]="module.id" [adaptImg]="false">
|
||||
</core-format-text>
|
||||
</h3>
|
||||
<ion-badge [color]="module.downloadStatus == statusDownloaded ? 'success' : 'light'"
|
||||
*ngIf="!module.calculatingSize && module.totalSize > 0">
|
||||
<ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus == statusDownloaded"
|
||||
[attr.aria-label]="'core.downloaded' | translate">
|
||||
</ion-icon>{{ module.totalSize | coreBytesToSize }}
|
||||
</ion-badge>
|
||||
<ion-badge color="light" *ngIf="module.calculatingSize">
|
||||
{{ 'core.calculating' | translate }}
|
||||
</ion-badge>
|
||||
</ion-label>
|
||||
<ion-card-content id="core-course-storage-section-{{section.id}}">
|
||||
<ng-container *ngIf="section.expanded">
|
||||
<ng-container *ngFor="let module of section.modules">
|
||||
<ion-item class="ion-no-padding core-course-storage-activity"
|
||||
*ngIf="downloadEnabled || (!module.calculatingSize && module.totalSize > 0)">
|
||||
<core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
|
||||
[modname]="module.modname" [componentId]="module.instance">
|
||||
</core-mod-icon>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h3 class="{{module.handlerData!.class}} addon-storagemanager-module-size">
|
||||
<core-format-text [text]="module.handlerData.title" [courseId]="module.course" contextLevel="module"
|
||||
[contextInstanceId]="module.id" [adaptImg]="false">
|
||||
</core-format-text>
|
||||
</h3>
|
||||
<ion-badge [color]="module.downloadStatus == statusDownloaded ? 'success' : 'light'"
|
||||
*ngIf="!module.calculatingSize && module.totalSize > 0">
|
||||
<ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus == statusDownloaded"
|
||||
[attr.aria-label]="'core.downloaded' | translate">
|
||||
</ion-icon>{{ module.totalSize | coreBytesToSize }}
|
||||
</ion-badge>
|
||||
<ion-badge color="light" *ngIf="module.calculatingSize">
|
||||
{{ 'core.calculating' | translate }}
|
||||
</ion-badge>
|
||||
</ion-label>
|
||||
|
||||
<div class="storage-buttons" slot="end">
|
||||
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
|
||||
module.downloadStatus != statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
|
||||
[canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
|
||||
(action)="prefetchModule(module, section)">
|
||||
</core-download-refresh>
|
||||
<ion-button fill="clear" (click)="deleteForModule(module, section)"
|
||||
*ngIf="!module.calculatingSize && module.totalSize > 0" color="danger">
|
||||
<ion-icon name="fas-trash" slot="icon-only"
|
||||
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
<div class="storage-buttons" slot="end">
|
||||
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
|
||||
module.downloadStatus != statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
|
||||
[canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
|
||||
(action)="prefetchModule(module, section)">
|
||||
</core-download-refresh>
|
||||
<ion-button fill="clear" (click)="deleteForModule(module, section)"
|
||||
*ngIf="!module.calculatingSize && module.totalSize > 0" color="danger">
|
||||
<ion-icon name="fas-trash" slot="icon-only"
|
||||
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
|
||||
import {
|
||||
CoreCourseHelper,
|
||||
|
@ -30,6 +30,7 @@ import { CoreSites } from '@services/sites';
|
|||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreDom } from '@singletons/dom';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
|
||||
/**
|
||||
|
@ -62,6 +63,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
|
||||
statusDownloaded = CoreConstants.DOWNLOADED;
|
||||
|
||||
protected initialSectionId?: number;
|
||||
protected siteUpdatedObserver?: CoreEventObserver;
|
||||
protected courseStatusObserver?: CoreEventObserver;
|
||||
protected sectionStatusObserver?: CoreEventObserver;
|
||||
|
@ -69,7 +71,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
protected isDestroyed = false;
|
||||
protected isGuest = false;
|
||||
|
||||
constructor() {
|
||||
constructor(protected elementRef: ElementRef) {
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||
|
@ -100,16 +102,19 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest');
|
||||
this.initialSectionId = CoreNavigator.getRouteNumberParam('sectionId');
|
||||
|
||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||
this.downloadEnabled = !CoreSites.getRequiredCurrentSite().isOfflineDisabled();
|
||||
|
||||
const sections = await CoreCourse.getSections(this.courseId, false, true);
|
||||
const sections = (await CoreCourse.getSections(this.courseId, false, true))
|
||||
.filter((section) => !CoreCourseHelper.isSectionStealth(section));
|
||||
this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
|
||||
.map(section => ({
|
||||
...section,
|
||||
totalSize: 0,
|
||||
calculatingSize: true,
|
||||
expanded: section.id === this.initialSectionId,
|
||||
modules: section.modules.map(module => ({
|
||||
...module,
|
||||
calculatingSize: true,
|
||||
|
@ -118,6 +123,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
|
||||
this.loaded = true;
|
||||
|
||||
CoreDom.scrollToElement(
|
||||
this.elementRef.nativeElement,
|
||||
'.core-course-storage-section-expanded',
|
||||
{ addYAxis: -10 },
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
this.initSizes(),
|
||||
this.initCoursePrefetch(),
|
||||
|
@ -641,6 +652,18 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle expand status.
|
||||
*
|
||||
* @param event Event object.
|
||||
* @param section Section to expand / collapse.
|
||||
*/
|
||||
toggleExpand(event: Event, section: AddonStorageManagerCourseSection): void {
|
||||
section.expanded = !section.expanded;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@ -663,6 +686,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
|
||||
totalSize: number;
|
||||
calculatingSize: boolean;
|
||||
expanded: boolean;
|
||||
modules: AddonStorageManagerModule[];
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
<core-navbar-buttons slot="end" prepend>
|
||||
<ion-button fill="clear" (click)="gotoCourseDownloads()" [attr.aria-label]="'addon.storagemanager.coursedownloads' | translate">
|
||||
<ion-icon name="fas-cloud-download-alt" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</core-navbar-buttons>
|
||||
<core-dynamic-component [component]="courseFormatComponent" [data]="data">
|
||||
<!-- Default course format. -->
|
||||
<core-loading [hideUntil]="loaded">
|
||||
|
|
|
@ -391,29 +391,38 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected section ID. If viewing all sections, use current scrolled section.
|
||||
*
|
||||
* @return Section ID, undefined if not found.
|
||||
*/
|
||||
protected async getSelectedSectionId(): Promise<number | undefined> {
|
||||
if (this.selectedSection?.id !== this.allSectionsId) {
|
||||
return this.selectedSection?.id;
|
||||
}
|
||||
|
||||
// Check current scrolled section.
|
||||
const allSectionElements: NodeListOf<HTMLElement> =
|
||||
this.elementRef.nativeElement.querySelectorAll('section.core-course-module-list-wrapper');
|
||||
|
||||
const scroll = await this.content.getScrollElement();
|
||||
const containerTop = scroll.getBoundingClientRect().top;
|
||||
|
||||
const element = Array.from(allSectionElements).find((element) => {
|
||||
const position = element.getBoundingClientRect();
|
||||
|
||||
// The bottom is inside the container or lower.
|
||||
return position.bottom >= containerTop;
|
||||
});
|
||||
|
||||
return Number(element?.getAttribute('id')) || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the course index modal.
|
||||
*/
|
||||
async openCourseIndex(): Promise<void> {
|
||||
let selectedId = this.selectedSection?.id;
|
||||
|
||||
if (selectedId == this.allSectionsId) {
|
||||
// Check current scrolled section.
|
||||
const allSectionElements: NodeListOf<HTMLElement> =
|
||||
this.elementRef.nativeElement.querySelectorAll('section.section-wrapper');
|
||||
|
||||
const scroll = await this.content.getScrollElement();
|
||||
const containerTop = scroll.getBoundingClientRect().top;
|
||||
|
||||
const element = Array.from(allSectionElements).find((element) => {
|
||||
const position = element.getBoundingClientRect();
|
||||
|
||||
// The bottom is inside the container or lower.
|
||||
return position.bottom >= containerTop;
|
||||
});
|
||||
|
||||
selectedId = Number(element?.getAttribute('id')) || undefined;
|
||||
}
|
||||
const selectedId = await this.getSelectedSectionId();
|
||||
|
||||
const data = await CoreDomUtils.openModal<CoreCourseIndexSectionWithModule>({
|
||||
component: CoreCourseCourseIndexComponent,
|
||||
|
@ -453,6 +462,23 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.moduleId = data.moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open course downloads page.
|
||||
*/
|
||||
async gotoCourseDownloads(): Promise<void> {
|
||||
const selectedId = await this.getSelectedSectionId();
|
||||
|
||||
CoreNavigator.navigateToSitePath(
|
||||
`storage/${this.course.id}`,
|
||||
{
|
||||
params: {
|
||||
title: this.course.fullname,
|
||||
sectionId: selectedId,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when selected section changes.
|
||||
*
|
||||
|
|
|
@ -48,34 +48,36 @@
|
|||
<ion-icon name="fas-eye-slash" *ngIf="!section.visible && section.uservisible" slot="end" class="restricted"
|
||||
[attr.aria-label]="'core.course.hiddenfromstudents' | translate"></ion-icon>
|
||||
</ion-item>
|
||||
<ng-container *ngIf="section.expanded">
|
||||
<ng-container *ngFor="let module of section.modules">
|
||||
<ion-item class="module" [class.item-dimmed]="!module.visible" [class.item-hightlighted]="section.highlighted"
|
||||
(click)="selectSectionOrModule($event, section.id, module.id)" button>
|
||||
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined"
|
||||
slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-icon class="completioninfo completion_incomplete" name="far-circle" *ngIf="module.completionStatus === 0"
|
||||
slot="start" [attr.aria-label]="'core.course.todo' | translate">
|
||||
</ion-icon>
|
||||
<ion-icon class="completioninfo completion_complete" name="fas-circle"
|
||||
*ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
|
||||
[attr.aria-label]="'core.course.done' | translate">
|
||||
</ion-icon>
|
||||
<ion-icon class="completioninfo completion_fail" name="fas-circle" *ngIf="module.completionStatus === 3"
|
||||
color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate">
|
||||
</ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="module.course">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-icon name="fas-lock" *ngIf="!module.uservisible" slot="end" class="restricted"
|
||||
[attr.aria-label]="'core.restricted' | translate"></ion-icon>
|
||||
</ion-item>
|
||||
<div id="core-course-index-section-{{section.id}}">
|
||||
<ng-container *ngIf="section.expanded">
|
||||
<ng-container *ngFor="let module of section.modules">
|
||||
<ion-item class="module" [class.item-dimmed]="!module.visible" [class.item-hightlighted]="section.highlighted"
|
||||
(click)="selectSectionOrModule($event, section.id, module.id)" button>
|
||||
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined"
|
||||
slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-icon class="completioninfo completion_incomplete" name="far-circle"
|
||||
*ngIf="module.completionStatus === 0" slot="start" [attr.aria-label]="'core.course.todo' | translate">
|
||||
</ion-icon>
|
||||
<ion-icon class="completioninfo completion_complete" name="fas-circle"
|
||||
*ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
|
||||
[attr.aria-label]="'core.course.done' | translate">
|
||||
</ion-icon>
|
||||
<ion-icon class="completioninfo completion_fail" name="fas-circle" *ngIf="module.completionStatus === 3"
|
||||
color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate">
|
||||
</ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="module.course">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-icon name="fas-lock" *ngIf="!module.uservisible" slot="end" class="restricted"
|
||||
[attr.aria-label]="'core.restricted' | translate"></ion-icon>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
<core-navbar-buttons slot="end" prepend>
|
||||
<ion-button fill="clear" (click)="gotoCourseDownloads()" [attr.aria-label]="'addon.storagemanager.coursedownloads' | translate">
|
||||
<ion-icon name="fas-cloud-download-alt" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</core-navbar-buttons>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!dataLoaded || !displayRefresher" (ionRefresh)="doRefresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
|
|
|
@ -366,14 +366,6 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
gotoCourseDownloads(): void {
|
||||
CoreNavigator.navigateToSitePath(
|
||||
`storage/${this.course.id}`,
|
||||
{ params: { title: this.course.fullname } },
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue