MOBILE-3914 course: Add side blocks component and replace it

main
Pau Ferrer Ocaña 2021-11-12 16:32:54 +01:00
parent 80f1b96f61
commit 8cd89ead85
17 changed files with 328 additions and 313 deletions

View File

@ -1399,6 +1399,8 @@
"core.areyousure": "moodle",
"core.back": "moodle",
"core.block.blocks": "moodle",
"core.block.noblocks": "error",
"core.block.opendrawerblocks": "moodle",
"core.browser": "local_moodlemobileapp",
"core.cancel": "moodle",
"core.cannotconnect": "local_moodlemobileapp",

View File

@ -72,7 +72,3 @@
height: auto;
}
}
:host-context(core-block-course-blocks) .core-empty-box {
position: relative;
}

View File

@ -16,15 +16,17 @@ import { NgModule } from '@angular/core';
import { CoreBlockComponent } from './block/block';
import { CoreBlockOnlyTitleComponent } from './only-title-block/only-title-block';
import { CoreBlockPreRenderedComponent } from './pre-rendered-block/pre-rendered-block';
import { CoreBlockCourseBlocksComponent } from './course-blocks/course-blocks';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreBlockSideBlocksComponent } from './side-blocks/side-blocks';
import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-blocks-button';
@NgModule({
declarations: [
CoreBlockComponent,
CoreBlockOnlyTitleComponent,
CoreBlockPreRenderedComponent,
CoreBlockCourseBlocksComponent,
CoreBlockSideBlocksComponent,
CoreBlockSideBlocksButtonComponent,
],
imports: [
CoreSharedModule,
@ -33,7 +35,8 @@ import { CoreSharedModule } from '@/core/shared.module';
CoreBlockComponent,
CoreBlockOnlyTitleComponent,
CoreBlockPreRenderedComponent,
CoreBlockCourseBlocksComponent,
CoreBlockSideBlocksComponent,
CoreBlockSideBlocksButtonComponent,
],
})
export class CoreBlockComponentsModule {}

View File

@ -1,15 +0,0 @@
<div class="core-course-blocks-content">
<ng-content></ng-content>
</div>
<div *ngIf="blocks && blocks.length > 0 && !hideBlocks" [class.core-hide-blocks]="hideBottomBlocks" class="core-course-blocks-side">
<core-loading [hideUntil]="dataLoaded" [fullscreen]="false">
<ion-list>
<!-- Course expand="block"s. -->
<ng-container *ngFor="let block of blocks">
<core-block *ngIf="block.visible" [block]="block" contextLevel="course" [instanceId]="courseId"
[extraData]="{'downloadEnabled': downloadEnabled}"></core-block>
</ng-container>
</ion-list>
</core-loading>
</div>

View File

@ -1,61 +0,0 @@
:host {
--side-blocks-box-shadow: var(--core-menu-box-shadow-start);
&.core-no-blocks .core-course-blocks-content {
height: auto;
}
&.core-has-blocks {
@media (min-width: 768px) {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
.core-course-blocks-content {
box-shadow: none !important;
flex-grow: 1;
max-width: 100%;
--ion-safe-area-right: 0px;
}
div.core-course-blocks-side {
max-width: var(--side-blocks-max-width);
min-width: var(--side-blocks-min-width);
box-shadow: var(--side-blocks-box-shadow);
z-index: 2;
}
.core-course-blocks-content,
div.core-course-blocks-side {
position: relative;
height: 100%;
.core-loading-center,
core-loading.core-loading-loaded {
position: initial;
}
}
}
@media (max-width: 767.98px) {
// Disable scroll on individual columns.
div.core-course-blocks-side {
height: auto;
&.core-hide-blocks {
display: none;
}
}
}
}
}
:host-context([dir="rtl"]).core-has-blocks {
@media (min-width: 768px) {
div.core-course-blocks-side {
box-shadow: var(--side-blocks-box-shadow);
}
}
}

View File

@ -0,0 +1,3 @@
<ion-button (click)="openBlocks()" [attr.aria-label]="'core.block.opendrawerblocks' | translate">
<ion-icon name="fas-cubes" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>

View File

@ -0,0 +1,29 @@
@import "~theme/globals";
:host {
@include position(50%, 0px, null, null);
position: fixed;
z-index: 10;
ion-button {
margin: 0;
--padding-start: 0.5em;
--padding-end: 0;
--border-radius: 2em 0 0 2em;
&::part(native) {
@include core-transition(padding, 200ms);
}
&:hover {
--padding-end: 1.2em;
--padding-start: 1em;
}
}
}
:host-context([dir=rtl]) {
ion-button {
--border-radius: 0 2em 2em 0;
}
}

View File

@ -0,0 +1,45 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreBlockSideBlocksComponent } from '../side-blocks/side-blocks';
/**
* Component that displays a button to open blocks.
*/
@Component({
selector: 'core-block-side-blocks-button',
templateUrl: 'side-blocks-button.html',
styleUrls: ['side-blocks-button.scss'],
})
export class CoreBlockSideBlocksButtonComponent {
@Input() courseId!: number;
@Input() downloadEnabled = false;
/**
* Open side blocks.
*/
openBlocks(): void {
CoreDomUtils.openSideModal({
component: CoreBlockSideBlocksComponent,
componentProps: {
courseId: this.courseId,
downloadEnabled: this.downloadEnabled,
},
});
}
}

View File

@ -0,0 +1,26 @@
<ion-header>
<ion-toolbar>
<h1>{{ 'core.block.blocks' | translate }}</h1>
<ion-buttons slot="end">
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-times" slot="icon-only" aria-hidden=true></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-list *ngIf="blocks.length > 0">
<ng-container *ngFor="let block of blocks">
<core-block *ngIf="block.visible" [block]="block" contextLevel="course" [instanceId]="courseId"
[extraData]="{'downloadEnabled': downloadEnabled}"></core-block>
</ng-container>
</ion-list>
<core-empty-box *ngIf="blocks.length == 0" icon="fas-cubes" [message]="'core.block.noblocks' | translate">
</core-empty-box>
</core-loading>
</ion-content>

View File

@ -12,50 +12,38 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef } from '@angular/core';
import { IonContent } from '@ionic/angular';
import { Component, ViewChildren, Input, OnInit, QueryList } from '@angular/core';
import { ModalController } from '@singletons';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourse, CoreCourseBlock } from '@features/course/services/course';
import { CoreBlockHelper } from '../../services/block-helper';
import { CoreBlockComponent } from '../block/block';
import { CoreUtils } from '@services/utils/utils';
import { IonRefresher } from '@ionic/angular';
/**
* Component that displays the list of course blocks.
* Component that displays the list of side blocks.
*/
@Component({
selector: 'core-block-course-blocks',
templateUrl: 'core-block-course-blocks.html',
styleUrls: ['course-blocks.scss'],
selector: 'core-block-side-blocks',
templateUrl: 'side-blocks.html',
})
export class CoreBlockCourseBlocksComponent implements OnInit {
export class CoreBlockSideBlocksComponent implements OnInit {
@Input() courseId!: number;
@Input() hideBlocks = false;
@Input() hideBottomBlocks = false;
@Input() downloadEnabled = false;
@ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>;
dataLoaded = false;
loaded = false;
blocks: CoreCourseBlock[] = [];
protected element: HTMLElement;
constructor(
element: ElementRef,
protected content: IonContent,
) {
this.element = element.nativeElement;
}
/**
* Component being initialized.
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.element.classList.add('core-no-blocks');
this.loadContent().finally(() => {
this.dataLoaded = true;
this.loaded = true;
});
}
@ -87,7 +75,6 @@ export class CoreBlockCourseBlocksComponent implements OnInit {
* @return Promise resolved when done.
*/
async loadContent(): Promise<void> {
try {
this.blocks = await CoreBlockHelper.getCourseBlocks(this.courseId);
} catch (error) {
@ -95,29 +82,26 @@ export class CoreBlockCourseBlocksComponent implements OnInit {
this.blocks = [];
}
const scrollElement = await this.content.getScrollElement();
if (!this.hideBlocks && this.blocks.length > 0) {
this.element.classList.add('core-has-blocks');
this.element.classList.remove('core-no-blocks');
scrollElement.classList.add('core-course-block-with-blocks');
} else {
this.element.classList.remove('core-has-blocks');
this.element.classList.add('core-no-blocks');
scrollElement.classList.remove('core-course-block-with-blocks');
}
}
/**
* Refresh data.
* Refresh the data.
*
* @return Promise resolved when done.
* @param refresher Refresher.
*/
async doRefresh(): Promise<void> {
async doRefresh(refresher?: IonRefresher): Promise<void> {
await CoreUtils.ignoreErrors(this.invalidateBlocks());
await this.loadContent();
await this.loadContent().finally(() => {
refresher?.complete();
});
}
/**
* Close modal.
*/
closeModal(): void {
ModalController.dismiss();
}
}

View File

@ -1,3 +1,5 @@
{
"blocks": "Blocks"
}
"blocks": "Blocks",
"noblocks": "No blocks found!",
"opendrawerblocks": "Open block drawer"
}

View File

@ -54,6 +54,22 @@ export class CoreBlockHelperProvider {
return blocks;
}
/**
* Returns if the course has any block.
*
* @param courseId Course ID.
* @return Wether course has blocks.
*/
async hasCourseBlocks(courseId: number): Promise<boolean> {
try {
const blocks = await this.getCourseBlocks(courseId);
return blocks.length > 0;
} catch {
return false;
}
}
}
export const CoreBlockHelper = makeSingleton(CoreBlockHelperProvider);

View File

@ -6,126 +6,115 @@
</core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<core-dynamic-component [component]="courseFormatComponent" [data]="data">
<!-- Default course format. -->
<core-loading [hideUntil]="loaded">
<!-- Section selector. -->
<core-dynamic-component [component]="sectionSelectorComponent" [data]="data">
<core-block-course-blocks *ngIf="loaded" [courseId]="course!.id" [hideBlocks]="!displayBlocks" [downloadEnabled]="downloadEnabled"
[hideBottomBlocks]="selectedSection && selectedSection.id == allSectionsId && canLoadMore">
<div *ngIf="displaySectionSelector && sections && hasSeveralSections"
class="ion-text-wrap ion-justify-content-between ion-align-items-center core-button-selector-row"
[class.core-section-download]="downloadEnabled">
<core-combobox [modalOptions]="sectionSelectorModalOptions" interface="modal" listboxId="core-course-section-button"
icon="fas-folder" [label]="'core.course.section' | translate"
[selection]="selectedSection ? selectedSection.name : 'core.course.sections' | translate"
(onChange)="sectionChanged($event)">
<span slot="text">
<core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course"
[contextInstanceId]="course?.id" [clean]="true" [singleLine]="true">
</core-format-text>
<ng-container *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</ng-container>
</span>
</core-combobox>
<!-- Section download. -->
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
</div>
</core-dynamic-component>
<core-dynamic-component [component]="courseFormatComponent" [data]="data">
<!-- Default course format. -->
<core-loading [hideUntil]="loaded">
<!-- Section selector. -->
<core-dynamic-component [component]="sectionSelectorComponent" [data]="data">
<div *ngIf="displaySectionSelector && sections && hasSeveralSections"
class="ion-text-wrap ion-justify-content-between ion-align-items-center core-button-selector-row"
[class.core-section-download]="downloadEnabled">
<core-combobox
[modalOptions]="sectionSelectorModalOptions"
interface="modal"
listboxId="core-course-section-button"
icon="fas-folder"
[label]="'core.course.section' | translate"
[selection]="selectedSection ? selectedSection.name : 'core.course.sections' | translate"
(onChange)="sectionChanged($event)"
>
<span slot="text">
<core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course"
[contextInstanceId]="course?.id" [clean]="true" [singleLine]="true">
</core-format-text>
<ng-container *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</ng-container>
</span>
</core-combobox>
<!-- Section download. -->
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
<!-- Course summary. By default we only display the course progress. -->
<core-dynamic-component [component]="courseSummaryComponent" [data]="data">
<ion-list lines="none" class="core-format-progress-list" *ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) ||
(selectedSection && selectedSection.id != allSectionsId &&
(selectedSection.availabilityinfo || selectedSection.visible === 0))">
<div *ngIf="imageThumb" class="core-course-thumb">
<img [src]="imageThumb" core-external-content alt="" />
</div>
<ng-container *ngIf="selectedSection">
<ion-item class="core-course-progress" *ngIf="selectedSection?.id == allSectionsId && progress !== undefined">
<core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
</core-progress-bar>
</ion-item>
<ion-item *ngIf="selectedSection && selectedSection.id != allSectionsId &&
(selectedSection.availabilityinfo || selectedSection.visible === 0)">
<ion-badge color="info" class="ion-text-wrap"
*ngIf="selectedSection.visible === 0 && selectedSection.uservisible !== false">
{{ 'core.course.hiddenfromstudents' | translate }}
</ion-badge>
<ion-badge color="info" class="ion-text-wrap"
*ngIf="selectedSection.visible === 0 && selectedSection.uservisible === false">
{{ 'core.notavailable' | translate }}
</ion-badge>
<ion-badge color="info" class="ion-text-wrap" *ngIf="selectedSection.availabilityinfo">
<core-format-text [text]="selectedSection.availabilityinfo" contextLevel="course"
[contextInstanceId]="course?.id">
</core-format-text>
</ion-badge>
</ion-item>
</ng-container>
</ion-list>
</core-dynamic-component>
<!-- Single section. -->
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
<core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-th-large"
[message]="'core.course.nocontentavailable' | translate">
</core-empty-box>
</core-dynamic-component>
</div>
<!-- Multiple sections. -->
<div *ngIf="selectedSection && selectedSection.id == allSectionsId">
<core-dynamic-component [component]="allSectionsComponent" [data]="data">
<ng-container *ngFor="let section of sections; index as i">
<ng-container *ngIf="i <= showSectionId">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container>
</ng-container>
</ng-container>
</core-dynamic-component>
<!-- Course summary. By default we only display the course progress. -->
<core-dynamic-component [component]="courseSummaryComponent" [data]="data">
<ion-list lines="none" class="core-format-progress-list"
*ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) ||
(selectedSection && selectedSection.id != allSectionsId &&
(selectedSection.availabilityinfo || selectedSection.visible === 0))">
<div *ngIf="imageThumb" class="core-course-thumb">
<img [src]="imageThumb" core-external-content alt=""/>
</div>
<ng-container *ngIf="selectedSection">
<ion-item class="core-course-progress"
*ngIf="selectedSection?.id == allSectionsId && progress !== undefined">
<core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
</core-progress-bar>
</ion-item>
<ion-item *ngIf="selectedSection && selectedSection.id != allSectionsId &&
(selectedSection.availabilityinfo || selectedSection.visible === 0)">
<ion-badge color="info" class="ion-text-wrap"
*ngIf="selectedSection.visible === 0 && selectedSection.uservisible !== false">
{{ 'core.course.hiddenfromstudents' | translate }}
</ion-badge>
<ion-badge color="info" class="ion-text-wrap"
*ngIf="selectedSection.visible === 0 && selectedSection.uservisible === false">
{{ 'core.notavailable' | translate }}
</ion-badge>
<ion-badge color="info" class="ion-text-wrap" *ngIf="selectedSection.availabilityinfo">
<core-format-text [text]="selectedSection.availabilityinfo" contextLevel="course"
[contextInstanceId]="course?.id">
</core-format-text>
</ion-badge>
</ion-item>
</ng-container>
</ion-list>
</core-dynamic-component>
<core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)"></core-infinite-loading>
</div>
<!-- Single section. -->
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
<core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-th-large"
[message]="'core.course.nocontentavailable' | translate">
</core-empty-box>
</core-dynamic-component>
</div>
<ion-buttons class="ion-padding core-course-section-nav-buttons safe-area-padding-horizontal"
*ngIf="displaySectionSelector && sections?.length">
<ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary"
[attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name">
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
<core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course" [contextInstanceId]="course?.id">
</core-format-text>
</ion-button>
<ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary"
[attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name">
<core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course" [contextInstanceId]="course?.id">
</core-format-text>
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
<!-- Multiple sections. -->
<div *ngIf="selectedSection && selectedSection.id == allSectionsId">
<core-dynamic-component [component]="allSectionsComponent" [data]="data">
<ng-container *ngFor="let section of sections; index as i">
<ng-container *ngIf="i <= showSectionId">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container>
</ng-container>
</ng-container>
</core-dynamic-component>
<core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)"></core-infinite-loading>
</div>
<ion-buttons class="ion-padding core-course-section-nav-buttons safe-area-padding-horizontal"
*ngIf="displaySectionSelector && sections?.length">
<ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary"
[attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name">
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
<core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course"
[contextInstanceId]="course?.id">
</core-format-text>
</ion-button>
<ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary"
[attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name">
<core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course"
[contextInstanceId]="course?.id">
</core-format-text>
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
</core-loading>
</core-dynamic-component>
</core-block-course-blocks>
<core-block-side-blocks-button *ngIf="course && displayBlocks && hasBlocks" [courseId]="course.id"
[downloadEnabled]="downloadEnabled">
</core-block-side-blocks-button>
</core-loading>
</core-dynamic-component>
<!-- Template to render a section. -->
<ng-template #sectionTemplate let-section="section">
<section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId">
<!-- Title is only displayed when viewing all sections. -->
<ion-item-divider *ngIf="selectedSection?.id == allSectionsId && section.name" class="ion-text-wrap" color="light"
[class.core-section-download]="downloadEnabled"
[class.item-dimmed]="section.visible === 0 || section.uservisible === false">
[class.core-section-download]="downloadEnabled" [class.item-dimmed]="section.visible === 0 || section.uservisible === false">
<ion-label>
<h2>
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">

View File

@ -24,7 +24,6 @@ import {
ViewChildren,
QueryList,
Type,
ViewChild,
ElementRef,
} from '@angular/core';
import { ModalOptions } from '@ionic/core';
@ -48,8 +47,8 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { IonContent, IonRefresher } from '@ionic/angular';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector';
import { CoreBlockHelper } from '@features/block/services/block-helper';
/**
* Component to display course contents using a certain format. If the format isn't found, use default one.
@ -79,7 +78,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when any module completion changes.
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>;
@ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent;
// All the possible component classes.
courseFormatComponent?: Type<unknown>;
@ -92,8 +90,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
showSectionId = 0;
data: Record<string, unknown> = {}; // Data to pass to the components.
displaySectionSelector?: boolean;
displayBlocks?: boolean;
displaySectionSelector = false;
displayBlocks = false;
hasBlocks = false;
selectedSection?: CoreCourseSection;
previousSection?: CoreCourseSection;
nextSection?: CoreCourseSection;
@ -180,7 +179,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise<void> {
this.setInputData();
this.sectionSelectorModalOptions.componentProps!.course = this.course;
this.sectionSelectorModalOptions.componentProps!.sections = this.sections;
@ -191,6 +190,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.displaySectionSelector = CoreCourseFormatDelegate.displaySectionSelector(this.course);
this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course);
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id);
this.updateProgress();
if ('overviewfiles' in this.course) {
@ -498,8 +500,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
await component.callComponentFunction('doRefresh', [refresher, done, afterCompletionChange]);
}) || [];
if (this.courseBlocksComponent) {
promises.push(this.courseBlocksComponent.doRefresh());
if (this.course) {
const courseId = this.course.id;
promises.push(CoreCourse.invalidateCourseBlocks(courseId).then(async () => {
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(courseId);
return;
}));
}
await Promise.all(promises);

View File

@ -3,11 +3,9 @@
<ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<core-context-menu>
<core-context-menu-item [priority]="1000" *ngIf="displayEnableDownload"
[content]="'core.settings.showdownloadoptions' | translate" (action)="switchDownload()"
iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
<core-context-menu-item [priority]="500"
[content]="'addon.storagemanager.managestorage' | translate"
<core-context-menu-item [priority]="1000" *ngIf="displayEnableDownload" [content]="'core.settings.showdownloadoptions' | translate"
(action)="switchDownload()" iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
<core-context-menu-item [priority]="500" [content]="'addon.storagemanager.managestorage' | translate"
(action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
@ -15,49 +13,52 @@
<ion-refresher slot="fixed" [disabled]="!dataLoaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-block-course-blocks [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled">
<core-loading [hideUntil]="dataLoaded">
<ion-list>
<!-- Site home main contents. -->
<ng-container *ngIf="section && section.hasContent">
<ion-item class="ion-text-wrap" *ngIf="section.summary">
<ion-label><core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId">
</core-format-text></ion-label>
</ion-item>
<core-loading [hideUntil]="dataLoaded">
<ion-list>
<!-- Site home main contents. -->
<ng-container *ngIf="section && section.hasContent">
<ion-item class="ion-text-wrap" *ngIf="section.summary">
<ion-label>
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId">
</core-format-text>
</ion-label>
</ion-item>
<core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId"
[downloadEnabled]="downloadEnabled" [section]="section"></core-course-module>
</ng-container>
<core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId"
[downloadEnabled]="downloadEnabled" [section]="section"></core-course-module>
</ng-container>
<!-- Site home items: news, categories, courses, etc. -->
<ng-container *ngIf="items.length > 0">
<core-spacer *ngIf="section && section!.hasContent"></core-spacer>
<ng-container *ngFor="let item of items">
<ng-container [ngSwitch]="item">
<ng-container *ngSwitchCase="'LIST_OF_COURSE'">
<ng-template *ngTemplateOutlet="allCourseList"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'LIST_OF_CATEGORIES'">
<ng-template *ngTemplateOutlet="categories"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'COURSE_SEARCH_BOX'">
<ng-template *ngTemplateOutlet="courseSearch"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'ENROLLED_COURSES'">
<ng-template *ngTemplateOutlet="enrolledCourseList"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'NEWS_ITEMS'">
<ng-template *ngTemplateOutlet="news"></ng-template>
</ng-container>
</ng-container>
<!-- Site home items: news, categories, courses, etc. -->
<ng-container *ngIf="items.length > 0">
<core-spacer *ngIf="section && section!.hasContent"></core-spacer>
<ng-container *ngFor="let item of items">
<ng-container [ngSwitch]="item">
<ng-container *ngSwitchCase="'LIST_OF_COURSE'">
<ng-template *ngTemplateOutlet="allCourseList"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'LIST_OF_CATEGORIES'">
<ng-template *ngTemplateOutlet="categories"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'COURSE_SEARCH_BOX'">
<ng-template *ngTemplateOutlet="courseSearch"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'ENROLLED_COURSES'">
<ng-template *ngTemplateOutlet="enrolledCourseList"></ng-template>
</ng-container>
<ng-container *ngSwitchCase="'NEWS_ITEMS'">
<ng-template *ngTemplateOutlet="news"></ng-template>
</ng-container>
</ng-container>
</ion-list>
<core-empty-box *ngIf="!hasContent" icon="fas-box-open" [message]="'core.course.nocontentavailable' | translate">
</ng-container>
</ng-container>
</ion-list>
<core-block-side-blocks-button *ngIf="hasBlocks" [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled">
</core-block-side-blocks-button>
</core-empty-box>
</core-loading>
</core-block-course-blocks>
<core-empty-box *ngIf="!hasContent" icon="fas-box-open" [message]="'core.course.nocontentavailable' | translate">
</core-empty-box>
</core-loading>
</ion-content>
<ng-template #allCourseList>
@ -88,13 +89,17 @@
<ion-item button class="ion-text-wrap" (click)="openMyCourses()" detail="true">
<ion-icon name="fas-graduation-cap" fixed-width slot="start" aria-hidden="true">
</ion-icon>
<ion-label><h2>{{ 'core.courses.mycourses' | translate}}</h2></ion-label>
<ion-label>
<h2>{{ 'core.courses.mycourses' | translate}}</h2>
</ion-label>
</ion-item>
</ng-template>
<ng-template #courseSearch>
<ion-item button class="ion-text-wrap" (click)="openSearch()" detail="true">
<ion-icon name="fas-search" slot="start" aria-hidden="true"></ion-icon>
<ion-label><h2>{{ 'core.courses.searchcourses' | translate}}</h2></ion-label>
<ion-label>
<h2>{{ 'core.courses.searchcourses' | translate}}</h2>
</ion-label>
</ion-item>
</ng-template>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { IonRefresher } from '@ionic/angular';
import { Params } from '@angular/router';
@ -24,10 +24,11 @@ import { CoreSiteHome } from '@features/sitehome/services/sitehome';
import { CoreCourses, CoreCoursesProvider } from '@features//courses/services/courses';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreCourseHelper, CoreCourseModule } from '@features/course/services/course-helper';
import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
import { CoreCourseModuleDelegate, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreNavigator } from '@services/navigator';
import { CoreBlockHelper } from '@features/block/services/block-helper';
import { CoreUtils } from '@services/utils/utils';
/**
* Page that displays site home index.
@ -38,14 +39,13 @@ import { CoreNavigator } from '@services/navigator';
})
export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
@ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent;
dataLoaded = false;
section?: CoreCourseWSSection & {
hasContent?: boolean;
};
hasContent = false;
hasBlocks = false;
items: string[] = [];
siteHomeId = 1;
currentSite!: CoreSite;
@ -106,8 +106,8 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
this.items = await CoreSiteHome.getFrontPageItems(config.frontpageloggedin);
this.hasContent = this.items.length > 0;
if (this.items.some((item) => item == 'NEWS_ITEMS')) {
// Get the news forum.
// Get the news forum.
if (this.items.includes('NEWS_ITEMS')) {
try {
const forum = await CoreSiteHome.getNewsForum(this.siteHomeId);
this.newsForumModule = await CoreCourse.getModule(forum.cmid, forum.course);
@ -140,17 +140,17 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
}
// Add log in Moodle.
CoreCourse.logView(
CoreUtils.ignoreErrors(CoreCourse.logView(
this.siteHomeId,
undefined,
undefined,
this.currentSite.getInfo()?.sitename,
).catch(() => {
// Ignore errors.
});
));
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true);
}
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.siteHomeId);
}
/**
@ -170,24 +170,15 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
return;
}));
promises.push(CoreCourse.invalidateCourseBlocks(this.siteHomeId));
if (this.section && this.section.modules) {
// Invalidate modules prefetch data.
promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId));
}
if (this.courseBlocksComponent) {
promises.push(this.courseBlocksComponent.invalidateBlocks());
}
Promise.all(promises).finally(async () => {
const p2: Promise<unknown>[] = [];
p2.push(this.loadContent());
if (this.courseBlocksComponent) {
p2.push(this.courseBlocksComponent.loadContent());
}
await Promise.all(p2).finally(() => {
await this.loadContent().finally(() => {
refresher?.complete();
});
});

View File

@ -197,13 +197,6 @@
--background: var(--core-progressbar-background);
}
--core-side-blocks-max-width: 30%;
--core-side-blocks-min-width: 280px;
core-block-course-blocks {
--side-blocks-max-width: var(--core-side-blocks-max-width);
--side-blocks-min-width: var(--core-side-blocks-min-width);
}
--ion-item-background: #{$ion-item-background};
--ion-item-detail-icon-color: var(--gray-darker);
--ion-item-detail-icon-font-size: 20px;