MOBILE-3914 course: Add side blocks component and replace it
parent
80f1b96f61
commit
8cd89ead85
|
@ -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",
|
||||
|
|
|
@ -72,7 +72,3 @@
|
|||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(core-block-course-blocks) .core-empty-box {
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
{
|
||||
"blocks": "Blocks"
|
||||
}
|
||||
"blocks": "Blocks",
|
||||
"noblocks": "No blocks found!",
|
||||
"opendrawerblocks": "Open block drawer"
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue