commit
						ce50a29975
					
				| @ -1399,6 +1399,8 @@ | |||||||
|   "core.areyousure": "moodle", |   "core.areyousure": "moodle", | ||||||
|   "core.back": "moodle", |   "core.back": "moodle", | ||||||
|   "core.block.blocks": "moodle", |   "core.block.blocks": "moodle", | ||||||
|  |   "core.block.noblocks": "error", | ||||||
|  |   "core.block.opendrawerblocks": "moodle", | ||||||
|   "core.browser": "local_moodlemobileapp", |   "core.browser": "local_moodlemobileapp", | ||||||
|   "core.cancel": "moodle", |   "core.cancel": "moodle", | ||||||
|   "core.cannotconnect": "local_moodlemobileapp", |   "core.cannotconnect": "local_moodlemobileapp", | ||||||
|  | |||||||
| @ -72,7 +72,3 @@ | |||||||
|         height: auto; |         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 { CoreBlockComponent } from './block/block'; | ||||||
| import { CoreBlockOnlyTitleComponent } from './only-title-block/only-title-block'; | import { CoreBlockOnlyTitleComponent } from './only-title-block/only-title-block'; | ||||||
| import { CoreBlockPreRenderedComponent } from './pre-rendered-block/pre-rendered-block'; | import { CoreBlockPreRenderedComponent } from './pre-rendered-block/pre-rendered-block'; | ||||||
| import { CoreBlockCourseBlocksComponent } from './course-blocks/course-blocks'; |  | ||||||
| import { CoreSharedModule } from '@/core/shared.module'; | import { CoreSharedModule } from '@/core/shared.module'; | ||||||
|  | import { CoreBlockSideBlocksComponent } from './side-blocks/side-blocks'; | ||||||
|  | import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-blocks-button'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
|         CoreBlockComponent, |         CoreBlockComponent, | ||||||
|         CoreBlockOnlyTitleComponent, |         CoreBlockOnlyTitleComponent, | ||||||
|         CoreBlockPreRenderedComponent, |         CoreBlockPreRenderedComponent, | ||||||
|         CoreBlockCourseBlocksComponent, |         CoreBlockSideBlocksComponent, | ||||||
|  |         CoreBlockSideBlocksButtonComponent, | ||||||
|     ], |     ], | ||||||
|     imports: [ |     imports: [ | ||||||
|         CoreSharedModule, |         CoreSharedModule, | ||||||
| @ -33,7 +35,8 @@ import { CoreSharedModule } from '@/core/shared.module'; | |||||||
|         CoreBlockComponent, |         CoreBlockComponent, | ||||||
|         CoreBlockOnlyTitleComponent, |         CoreBlockOnlyTitleComponent, | ||||||
|         CoreBlockPreRenderedComponent, |         CoreBlockPreRenderedComponent, | ||||||
|         CoreBlockCourseBlocksComponent, |         CoreBlockSideBlocksComponent, | ||||||
|  |         CoreBlockSideBlocksButtonComponent, | ||||||
|     ], |     ], | ||||||
| }) | }) | ||||||
| export class CoreBlockComponentsModule {} | 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,39 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef } from '@angular/core'; | import { Component, ViewChildren, Input, OnInit, QueryList } from '@angular/core'; | ||||||
| import { IonContent } from '@ionic/angular'; | import { ModalController } from '@singletons'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreCourse, CoreCourseBlock } from '@features/course/services/course'; | import { CoreCourse, CoreCourseBlock } from '@features/course/services/course'; | ||||||
| import { CoreBlockHelper } from '../../services/block-helper'; | import { CoreBlockHelper } from '../../services/block-helper'; | ||||||
| import { CoreBlockComponent } from '../block/block'; | import { CoreBlockComponent } from '../block/block'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { IonRefresher } from '@ionic/angular'; | ||||||
|  | import { CoreCoursesDashboard } from '@features/courses/services/dashboard'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Component that displays the list of course blocks. |  * Component that displays the list of side blocks. | ||||||
|  */ |  */ | ||||||
| @Component({ | @Component({ | ||||||
|     selector: 'core-block-course-blocks', |     selector: 'core-block-side-blocks', | ||||||
|     templateUrl: 'core-block-course-blocks.html', |     templateUrl: 'side-blocks.html', | ||||||
|     styleUrls: ['course-blocks.scss'], |  | ||||||
| }) | }) | ||||||
| export class CoreBlockCourseBlocksComponent implements OnInit { | export class CoreBlockSideBlocksComponent implements OnInit { | ||||||
| 
 | 
 | ||||||
|     @Input() courseId!: number; |     @Input() courseId?: number; | ||||||
|     @Input() hideBlocks = false; |  | ||||||
|     @Input() hideBottomBlocks = false; |  | ||||||
|     @Input() downloadEnabled = false; |     @Input() downloadEnabled = false; | ||||||
| 
 | 
 | ||||||
|     @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>; |     @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>; | ||||||
| 
 | 
 | ||||||
|     dataLoaded = false; |     loaded = false; | ||||||
|     blocks: CoreCourseBlock[] = []; |     blocks: CoreCourseBlock[] = []; | ||||||
| 
 | 
 | ||||||
|     protected element: HTMLElement; |  | ||||||
| 
 |  | ||||||
|     constructor( |  | ||||||
|         element: ElementRef, |  | ||||||
|         protected content: IonContent, |  | ||||||
|     ) { |  | ||||||
|         this.element = element.nativeElement; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Component being initialized. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     async ngOnInit(): Promise<void> { |     async ngOnInit(): Promise<void> { | ||||||
|         this.element.classList.add('core-no-blocks'); |  | ||||||
|         this.loadContent().finally(() => { |         this.loadContent().finally(() => { | ||||||
|             this.dataLoaded = true; |             this.loaded = true; | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -67,8 +56,10 @@ export class CoreBlockCourseBlocksComponent implements OnInit { | |||||||
|     async invalidateBlocks(): Promise<void> { |     async invalidateBlocks(): Promise<void> { | ||||||
|         const promises: Promise<void>[] = []; |         const promises: Promise<void>[] = []; | ||||||
| 
 | 
 | ||||||
|         if (CoreBlockHelper.canGetCourseBlocks()) { |         if (this.courseId) { | ||||||
|             promises.push(CoreCourse.invalidateCourseBlocks(this.courseId)); |             promises.push(CoreCourse.invalidateCourseBlocks(this.courseId)); | ||||||
|  |         } else { | ||||||
|  |             promises.push(CoreCoursesDashboard.invalidateDashboardBlocks()); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Invalidate the blocks.
 |         // Invalidate the blocks.
 | ||||||
| @ -87,37 +78,39 @@ export class CoreBlockCourseBlocksComponent implements OnInit { | |||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     async loadContent(): Promise<void> { |     async loadContent(): Promise<void> { | ||||||
| 
 |  | ||||||
|         try { |         try { | ||||||
|  |             if (this.courseId) { | ||||||
|                 this.blocks = await CoreBlockHelper.getCourseBlocks(this.courseId); |                 this.blocks = await CoreBlockHelper.getCourseBlocks(this.courseId); | ||||||
|  |             } else { | ||||||
|  |                 const blocks = await CoreCoursesDashboard.getDashboardBlocks(); | ||||||
|  | 
 | ||||||
|  |                 this.blocks = blocks.sideBlocks; | ||||||
|  |             } | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             CoreDomUtils.showErrorModal(error); |             CoreDomUtils.showErrorModal(error); | ||||||
| 
 | 
 | ||||||
|             this.blocks = []; |             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 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; |         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); | export const CoreBlockHelper = makeSingleton(CoreBlockHelperProvider); | ||||||
|  | |||||||
| @ -6,10 +6,6 @@ | |||||||
|         </core-context-menu-item> |         </core-context-menu-item> | ||||||
|     </core-context-menu> |     </core-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| 
 |  | ||||||
| <core-block-course-blocks *ngIf="loaded" [courseId]="course!.id" [hideBlocks]="!displayBlocks" [downloadEnabled]="downloadEnabled" |  | ||||||
|     [hideBottomBlocks]="selectedSection && selectedSection.id == allSectionsId && canLoadMore"> |  | ||||||
| 
 |  | ||||||
| <core-dynamic-component [component]="courseFormatComponent" [data]="data"> | <core-dynamic-component [component]="courseFormatComponent" [data]="data"> | ||||||
|     <!-- Default course format. --> |     <!-- Default course format. --> | ||||||
|     <core-loading [hideUntil]="loaded"> |     <core-loading [hideUntil]="loaded"> | ||||||
| @ -19,15 +15,10 @@ | |||||||
|             <div *ngIf="displaySectionSelector && sections && hasSeveralSections" |             <div *ngIf="displaySectionSelector && sections && hasSeveralSections" | ||||||
|                 class="ion-text-wrap ion-justify-content-between ion-align-items-center core-button-selector-row" |                 class="ion-text-wrap ion-justify-content-between ion-align-items-center core-button-selector-row" | ||||||
|                 [class.core-section-download]="downloadEnabled"> |                 [class.core-section-download]="downloadEnabled"> | ||||||
|                     <core-combobox |                 <core-combobox [modalOptions]="sectionSelectorModalOptions" interface="modal" listboxId="core-course-section-button" | ||||||
|                         [modalOptions]="sectionSelectorModalOptions" |                     icon="fas-folder" [label]="'core.course.section' | translate" | ||||||
|                         interface="modal" |  | ||||||
|                         listboxId="core-course-section-button" |  | ||||||
|                         icon="fas-folder" |  | ||||||
|                         [label]="'core.course.section' | translate" |  | ||||||
|                     [selection]="selectedSection ? selectedSection.name : 'core.course.sections' | translate" |                     [selection]="selectedSection ? selectedSection.name : 'core.course.sections' | translate" | ||||||
|                         (onChange)="sectionChanged($event)" |                     (onChange)="sectionChanged($event)"> | ||||||
|                     > |  | ||||||
|                     <span slot="text"> |                     <span slot="text"> | ||||||
|                         <core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course" |                         <core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course" | ||||||
|                             [contextInstanceId]="course?.id" [clean]="true" [singleLine]="true"> |                             [contextInstanceId]="course?.id" [clean]="true" [singleLine]="true"> | ||||||
| @ -42,16 +33,14 @@ | |||||||
| 
 | 
 | ||||||
|         <!-- Course summary. By default we only display the course progress. --> |         <!-- Course summary. By default we only display the course progress. --> | ||||||
|         <core-dynamic-component [component]="courseSummaryComponent" [data]="data"> |         <core-dynamic-component [component]="courseSummaryComponent" [data]="data"> | ||||||
|                 <ion-list lines="none" class="core-format-progress-list" |             <ion-list lines="none" class="core-format-progress-list" *ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) || | ||||||
|                     *ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) || |  | ||||||
|                     (selectedSection && selectedSection.id != allSectionsId && |                     (selectedSection && selectedSection.id != allSectionsId && | ||||||
|                     (selectedSection.availabilityinfo || selectedSection.visible === 0))"> |                     (selectedSection.availabilityinfo || selectedSection.visible === 0))"> | ||||||
|                 <div *ngIf="imageThumb" class="core-course-thumb"> |                 <div *ngIf="imageThumb" class="core-course-thumb"> | ||||||
|                     <img [src]="imageThumb" core-external-content alt="" /> |                     <img [src]="imageThumb" core-external-content alt="" /> | ||||||
|                 </div> |                 </div> | ||||||
|                 <ng-container *ngIf="selectedSection"> |                 <ng-container *ngIf="selectedSection"> | ||||||
|                         <ion-item class="core-course-progress" |                     <ion-item class="core-course-progress" *ngIf="selectedSection?.id == allSectionsId && progress !== undefined"> | ||||||
|                             *ngIf="selectedSection?.id == allSectionsId && progress !== undefined"> |  | ||||||
|                         <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress"> |                         <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress"> | ||||||
|                         </core-progress-bar> |                         </core-progress-bar> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| @ -103,29 +92,29 @@ | |||||||
|             <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary" |             <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary" | ||||||
|                 [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name"> |                 [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name"> | ||||||
|                 <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon> |                 <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" |                 <core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course" [contextInstanceId]="course?.id"> | ||||||
|                         [contextInstanceId]="course?.id"> |  | ||||||
|                 </core-format-text> |                 </core-format-text> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|             <ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary" |             <ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary" | ||||||
|                 [attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name"> |                 [attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name"> | ||||||
|                     <core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course" |                 <core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course" [contextInstanceId]="course?.id"> | ||||||
|                         [contextInstanceId]="course?.id"> |  | ||||||
|                 </core-format-text> |                 </core-format-text> | ||||||
|                 <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|  | 
 | ||||||
|  |         <core-block-side-blocks-button *ngIf="course && displayBlocks && hasBlocks" [courseId]="course.id" | ||||||
|  |             [downloadEnabled]="downloadEnabled"> | ||||||
|  |         </core-block-side-blocks-button> | ||||||
|     </core-loading> |     </core-loading> | ||||||
| </core-dynamic-component> | </core-dynamic-component> | ||||||
| </core-block-course-blocks> |  | ||||||
| 
 | 
 | ||||||
| <!-- Template to render a section. --> | <!-- Template to render a section. --> | ||||||
| <ng-template #sectionTemplate let-section="section"> | <ng-template #sectionTemplate let-section="section"> | ||||||
|     <section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId"> |     <section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId"> | ||||||
|         <!-- Title is only displayed when viewing all sections. --> |         <!-- Title is only displayed when viewing all sections. --> | ||||||
|         <ion-item-divider *ngIf="selectedSection?.id == allSectionsId && section.name" class="ion-text-wrap" color="light" |         <ion-item-divider *ngIf="selectedSection?.id == allSectionsId && section.name" class="ion-text-wrap" color="light" | ||||||
|             [class.core-section-download]="downloadEnabled" |             [class.core-section-download]="downloadEnabled" [class.item-dimmed]="section.visible === 0 || section.uservisible === false"> | ||||||
|             [class.item-dimmed]="section.visible === 0 || section.uservisible === false"> |  | ||||||
|             <ion-label> |             <ion-label> | ||||||
|                 <h2> |                 <h2> | ||||||
|                     <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> |                     <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> | ||||||
|  | |||||||
| @ -24,7 +24,6 @@ import { | |||||||
|     ViewChildren, |     ViewChildren, | ||||||
|     QueryList, |     QueryList, | ||||||
|     Type, |     Type, | ||||||
|     ViewChild, |  | ||||||
|     ElementRef, |     ElementRef, | ||||||
| } from '@angular/core'; | } from '@angular/core'; | ||||||
| import { ModalOptions } from '@ionic/core'; | import { ModalOptions } from '@ionic/core'; | ||||||
| @ -48,8 +47,8 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events'; | |||||||
| import { IonContent, IonRefresher } from '@ionic/angular'; | import { IonContent, IonRefresher } from '@ionic/angular'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; | 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 { 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. |  * 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.
 |     @Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when any module completion changes.
 | ||||||
| 
 | 
 | ||||||
|     @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>; |     @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>; | ||||||
|     @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent; |  | ||||||
| 
 | 
 | ||||||
|     // All the possible component classes.
 |     // All the possible component classes.
 | ||||||
|     courseFormatComponent?: Type<unknown>; |     courseFormatComponent?: Type<unknown>; | ||||||
| @ -92,8 +90,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|     showSectionId = 0; |     showSectionId = 0; | ||||||
|     data: Record<string, unknown> = {}; // Data to pass to the components.
 |     data: Record<string, unknown> = {}; // Data to pass to the components.
 | ||||||
| 
 | 
 | ||||||
|     displaySectionSelector?: boolean; |     displaySectionSelector = false; | ||||||
|     displayBlocks?: boolean; |     displayBlocks = false; | ||||||
|  |     hasBlocks = false; | ||||||
|     selectedSection?: CoreCourseSection; |     selectedSection?: CoreCourseSection; | ||||||
|     previousSection?: CoreCourseSection; |     previousSection?: CoreCourseSection; | ||||||
|     nextSection?: CoreCourseSection; |     nextSection?: CoreCourseSection; | ||||||
| @ -180,7 +179,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|     /** |     /** | ||||||
|      * Detect changes on input properties. |      * Detect changes on input properties. | ||||||
|      */ |      */ | ||||||
|     ngOnChanges(changes: { [name: string]: SimpleChange }): void { |     async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise<void> { | ||||||
|         this.setInputData(); |         this.setInputData(); | ||||||
|         this.sectionSelectorModalOptions.componentProps!.course = this.course; |         this.sectionSelectorModalOptions.componentProps!.course = this.course; | ||||||
|         this.sectionSelectorModalOptions.componentProps!.sections = this.sections; |         this.sectionSelectorModalOptions.componentProps!.sections = this.sections; | ||||||
| @ -191,6 +190,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
| 
 | 
 | ||||||
|             this.displaySectionSelector = CoreCourseFormatDelegate.displaySectionSelector(this.course); |             this.displaySectionSelector = CoreCourseFormatDelegate.displaySectionSelector(this.course); | ||||||
|             this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course); |             this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course); | ||||||
|  | 
 | ||||||
|  |             this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id); | ||||||
|  | 
 | ||||||
|             this.updateProgress(); |             this.updateProgress(); | ||||||
| 
 | 
 | ||||||
|             if ('overviewfiles' in this.course) { |             if ('overviewfiles' in this.course) { | ||||||
| @ -498,8 +500,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|             await component.callComponentFunction('doRefresh', [refresher, done, afterCompletionChange]); |             await component.callComponentFunction('doRefresh', [refresher, done, afterCompletionChange]); | ||||||
|         }) || []; |         }) || []; | ||||||
| 
 | 
 | ||||||
|         if (this.courseBlocksComponent) { |         if (this.course) { | ||||||
|             promises.push(this.courseBlocksComponent.doRefresh()); |             const courseId = this.course.id; | ||||||
|  |             promises.push(CoreCourse.invalidateCourseBlocks(courseId).then(async () => { | ||||||
|  |                 this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(courseId); | ||||||
|  | 
 | ||||||
|  |                 return; | ||||||
|  |             })); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         await Promise.all(promises); |         await Promise.all(promises); | ||||||
|  | |||||||
| @ -4,10 +4,9 @@ | |||||||
|     </ion-button> |     </ion-button> | ||||||
|     <core-context-menu> |     <core-context-menu> | ||||||
|         <core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="1000" |         <core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="1000" | ||||||
|             [content]="'core.settings.showdownloadoptions' | translate" (action)="switchDownload()" |             [content]="'core.settings.showdownloadoptions' | translate" (action)="switchDownload()" iconAction="toggle" | ||||||
|             iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item> |             [(toggle)]="downloadEnabled"></core-context-menu-item> | ||||||
|         <core-context-menu-item [priority]="500" |         <core-context-menu-item [priority]="500" [content]="'addon.storagemanager.managestorage' | translate" | ||||||
|             [content]="'addon.storagemanager.managestorage' | translate" |  | ||||||
|             (action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item> |             (action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item> | ||||||
|     </core-context-menu> |     </core-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| @ -24,7 +23,9 @@ | |||||||
|             </ng-container> |             </ng-container> | ||||||
|         </ion-list> |         </ion-list> | ||||||
| 
 | 
 | ||||||
|         <core-empty-box *ngIf="blocks.length == 0" icon="fas-th-large" [message]="'core.course.nocontentavailable' | translate"> |         <core-block-side-blocks-button *ngIf="hasSideBlocks" [downloadEnabled]="downloadEnabled"></core-block-side-blocks-button> | ||||||
|  | 
 | ||||||
|  |         <core-empty-box *ngIf="blocks.length == 0" icon="fas-cubes" [message]="'core.course.nocontentavailable' | translate"> | ||||||
|         </core-empty-box> |         </core-empty-box> | ||||||
|     </core-loading> |     </core-loading> | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ import { CoreDomUtils } from '@services/utils/dom'; | |||||||
| import { CoreCourseBlock } from '@features/course/services/course'; | import { CoreCourseBlock } from '@features/course/services/course'; | ||||||
| import { CoreBlockComponent } from '@features/block/components/block/block'; | import { CoreBlockComponent } from '@features/block/components/block/block'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
|  | import { CoreBlockDelegate } from '@features/block/services/block-delegate'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Page that displays the dashboard page. |  * Page that displays the dashboard page. | ||||||
| @ -36,6 +37,7 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|     @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>; |     @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>; | ||||||
| 
 | 
 | ||||||
|  |     hasSideBlocks = false; | ||||||
|     searchEnabled = false; |     searchEnabled = false; | ||||||
|     downloadEnabled = false; |     downloadEnabled = false; | ||||||
|     downloadCourseEnabled = false; |     downloadCourseEnabled = false; | ||||||
| @ -88,7 +90,11 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { | |||||||
|             this.userId = CoreSites.getCurrentSiteUserId(); |             this.userId = CoreSites.getCurrentSiteUserId(); | ||||||
| 
 | 
 | ||||||
|             try { |             try { | ||||||
|                 this.blocks = await CoreCoursesDashboard.getDashboardBlocks(); |                 const blocks = await CoreCoursesDashboard.getDashboardBlocks(); | ||||||
|  | 
 | ||||||
|  |                 this.blocks = blocks.mainBlocks; | ||||||
|  | 
 | ||||||
|  |                 this.hasSideBlocks = CoreBlockDelegate.hasSupportedBlock(blocks.sideBlocks); | ||||||
|             } catch (error) { |             } catch (error) { | ||||||
|                 CoreDomUtils.showErrorModal(error); |                 CoreDomUtils.showErrorModal(error); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -38,14 +38,14 @@ export class CoreCoursesDashboardProvider { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get dashboard blocks. |      * Get dashboard blocks from WS. | ||||||
|      * |      * | ||||||
|      * @param userId User ID. Default, current user. |      * @param userId User ID. Default, current user. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved with the list of blocks. |      * @return Promise resolved with the list of blocks. | ||||||
|      * @since 3.6 |      * @since 3.6 | ||||||
|      */ |      */ | ||||||
|     async getDashboardBlocks(userId?: number, siteId?: string): Promise<CoreCourseBlock[]> { |     protected async getDashboardBlocksFromWS(userId?: number, siteId?: string): Promise<CoreCourseBlock[]> { | ||||||
|         const site = await CoreSites.getSite(siteId); |         const site = await CoreSites.getSite(siteId); | ||||||
| 
 | 
 | ||||||
|         const params: CoreBlockGetDashboardBlocksWSParams = { |         const params: CoreBlockGetDashboardBlocksWSParams = { | ||||||
| @ -63,6 +63,44 @@ export class CoreCoursesDashboardProvider { | |||||||
|         return result.blocks || []; |         return result.blocks || []; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Get dashboard blocks. | ||||||
|  |      * | ||||||
|  |      * @param userId User ID. Default, current user. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved with the list of blocks. | ||||||
|  |      */ | ||||||
|  |     async getDashboardBlocks(userId?: number, siteId?: string): Promise<CoreCoursesDashboardBlocks> { | ||||||
|  |         const blocks = await CoreCoursesDashboard.getDashboardBlocksFromWS(userId, siteId); | ||||||
|  | 
 | ||||||
|  |         let mainBlocks: CoreCourseBlock[] = []; | ||||||
|  |         let sideBlocks: CoreCourseBlock[] = []; | ||||||
|  | 
 | ||||||
|  |         blocks.forEach((block) => { | ||||||
|  |             if (block.region == 'content' || block.region == 'main') { | ||||||
|  |                 mainBlocks.push(block); | ||||||
|  |             } else { | ||||||
|  |                 sideBlocks.push(block); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         if (mainBlocks.length == 0) { | ||||||
|  |             mainBlocks = []; | ||||||
|  |             sideBlocks = []; | ||||||
|  | 
 | ||||||
|  |             blocks.forEach((block) => { | ||||||
|  |                 if (block.region.match('side')) { | ||||||
|  |                     sideBlocks.push(block); | ||||||
|  |                 } else { | ||||||
|  |                     mainBlocks.push(block); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return { mainBlocks, sideBlocks }; | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Invalidates dashboard blocks WS call. |      * Invalidates dashboard blocks WS call. | ||||||
|      * |      * | ||||||
| @ -122,6 +160,11 @@ export class CoreCoursesDashboardProvider { | |||||||
| 
 | 
 | ||||||
| export const CoreCoursesDashboard = makeSingleton(CoreCoursesDashboardProvider); | export const CoreCoursesDashboard = makeSingleton(CoreCoursesDashboardProvider); | ||||||
| 
 | 
 | ||||||
|  | export type CoreCoursesDashboardBlocks = { | ||||||
|  |     mainBlocks: CoreCourseBlock[]; | ||||||
|  |     sideBlocks: CoreCourseBlock[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Params of core_block_get_dashboard_blocks WS. |  * Params of core_block_get_dashboard_blocks WS. | ||||||
|  */ |  */ | ||||||
|  | |||||||
| @ -68,7 +68,7 @@ export class CoreDashboardHomeHandlerService implements CoreMainMenuHomeHandler | |||||||
|         if (dashboardAvailable && blocksEnabled) { |         if (dashboardAvailable && blocksEnabled) { | ||||||
|             const blocks = await CoreCoursesDashboard.getDashboardBlocks(undefined, siteId); |             const blocks = await CoreCoursesDashboard.getDashboardBlocks(undefined, siteId); | ||||||
| 
 | 
 | ||||||
|             return CoreBlockDelegate.hasSupportedBlock(blocks); |             return CoreBlockDelegate.hasSupportedBlock(blocks.mainBlocks) || CoreBlockDelegate.hasSupportedBlock(blocks.sideBlocks); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Check if my overview is enabled. If it's enabled we will fake enabled blocks.
 |         // Check if my overview is enabled. If it's enabled we will fake enabled blocks.
 | ||||||
|  | |||||||
| @ -3,11 +3,9 @@ | |||||||
|         <ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon> |         <ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon> | ||||||
|     </ion-button> |     </ion-button> | ||||||
|     <core-context-menu> |     <core-context-menu> | ||||||
|         <core-context-menu-item [priority]="1000" *ngIf="displayEnableDownload" |         <core-context-menu-item [priority]="1000" *ngIf="displayEnableDownload" [content]="'core.settings.showdownloadoptions' | translate" | ||||||
|             [content]="'core.settings.showdownloadoptions' | translate" (action)="switchDownload()" |             (action)="switchDownload()" iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item> | ||||||
|             iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item> |         <core-context-menu-item [priority]="500" [content]="'addon.storagemanager.managestorage' | translate" | ||||||
|         <core-context-menu-item [priority]="500" |  | ||||||
|             [content]="'addon.storagemanager.managestorage' | translate" |  | ||||||
|             (action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item> |             (action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item> | ||||||
|     </core-context-menu> |     </core-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| @ -15,14 +13,15 @@ | |||||||
|     <ion-refresher slot="fixed" [disabled]="!dataLoaded" (ionRefresh)="doRefresh($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!dataLoaded" (ionRefresh)="doRefresh($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-block-course-blocks [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled"> |  | ||||||
|     <core-loading [hideUntil]="dataLoaded"> |     <core-loading [hideUntil]="dataLoaded"> | ||||||
|         <ion-list> |         <ion-list> | ||||||
|             <!-- Site home main contents. --> |             <!-- Site home main contents. --> | ||||||
|             <ng-container *ngIf="section && section.hasContent"> |             <ng-container *ngIf="section && section.hasContent"> | ||||||
|                 <ion-item class="ion-text-wrap" *ngIf="section.summary"> |                 <ion-item class="ion-text-wrap" *ngIf="section.summary"> | ||||||
|                             <ion-label><core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId"> |                     <ion-label> | ||||||
|                             </core-format-text></ion-label> |                         <core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId"> | ||||||
|  |                         </core-format-text> | ||||||
|  |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
| 
 | 
 | ||||||
|                 <core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId" |                 <core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId" | ||||||
| @ -53,11 +52,13 @@ | |||||||
|                 </ng-container> |                 </ng-container> | ||||||
|             </ng-container> |             </ng-container> | ||||||
|         </ion-list> |         </ion-list> | ||||||
|  |         <core-block-side-blocks-button *ngIf="hasBlocks" [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled"> | ||||||
|  |         </core-block-side-blocks-button> | ||||||
|  | 
 | ||||||
|         <core-empty-box *ngIf="!hasContent" icon="fas-box-open" [message]="'core.course.nocontentavailable' | translate"> |         <core-empty-box *ngIf="!hasContent" icon="fas-box-open" [message]="'core.course.nocontentavailable' | translate"> | ||||||
| 
 | 
 | ||||||
|         </core-empty-box> |         </core-empty-box> | ||||||
|     </core-loading> |     </core-loading> | ||||||
|     </core-block-course-blocks> |  | ||||||
| </ion-content> | </ion-content> | ||||||
| 
 | 
 | ||||||
| <ng-template #allCourseList> | <ng-template #allCourseList> | ||||||
| @ -88,13 +89,17 @@ | |||||||
|     <ion-item button class="ion-text-wrap" (click)="openMyCourses()" detail="true"> |     <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 name="fas-graduation-cap" fixed-width slot="start" aria-hidden="true"> | ||||||
|         </ion-icon> |         </ion-icon> | ||||||
|         <ion-label><h2>{{ 'core.courses.mycourses' | translate}}</h2></ion-label> |         <ion-label> | ||||||
|  |             <h2>{{ 'core.courses.mycourses' | translate}}</h2> | ||||||
|  |         </ion-label> | ||||||
|     </ion-item> |     </ion-item> | ||||||
| </ng-template> | </ng-template> | ||||||
| 
 | 
 | ||||||
| <ng-template #courseSearch> | <ng-template #courseSearch> | ||||||
|     <ion-item button class="ion-text-wrap" (click)="openSearch()" detail="true"> |     <ion-item button class="ion-text-wrap" (click)="openSearch()" detail="true"> | ||||||
|         <ion-icon name="fas-search" slot="start" aria-hidden="true"></ion-icon> |         <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> |     </ion-item> | ||||||
| </ng-template> | </ng-template> | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // 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 { IonRefresher } from '@ionic/angular'; | ||||||
| import { Params } from '@angular/router'; | 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 { CoreCourses, CoreCoursesProvider } from '@features//courses/services/courses'; | ||||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||||
| import { CoreCourseHelper, CoreCourseModule } from '@features/course/services/course-helper'; | 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 { CoreCourseModuleDelegate, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; | ||||||
| import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; | import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | 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. |  * Page that displays site home index. | ||||||
| @ -38,14 +39,13 @@ import { CoreNavigator } from '@services/navigator'; | |||||||
| }) | }) | ||||||
| export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { | export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { | ||||||
| 
 | 
 | ||||||
|     @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent; |  | ||||||
| 
 |  | ||||||
|     dataLoaded = false; |     dataLoaded = false; | ||||||
|     section?: CoreCourseWSSection & { |     section?: CoreCourseWSSection & { | ||||||
|         hasContent?: boolean; |         hasContent?: boolean; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     hasContent = false; |     hasContent = false; | ||||||
|  |     hasBlocks = false; | ||||||
|     items: string[] = []; |     items: string[] = []; | ||||||
|     siteHomeId = 1; |     siteHomeId = 1; | ||||||
|     currentSite!: CoreSite; |     currentSite!: CoreSite; | ||||||
| @ -106,8 +106,8 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { | |||||||
|         this.items = await CoreSiteHome.getFrontPageItems(config.frontpageloggedin); |         this.items = await CoreSiteHome.getFrontPageItems(config.frontpageloggedin); | ||||||
|         this.hasContent = this.items.length > 0; |         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 { |             try { | ||||||
|                 const forum = await CoreSiteHome.getNewsForum(this.siteHomeId); |                 const forum = await CoreSiteHome.getNewsForum(this.siteHomeId); | ||||||
|                 this.newsForumModule = await CoreCourse.getModule(forum.cmid, forum.course); |                 this.newsForumModule = await CoreCourse.getModule(forum.cmid, forum.course); | ||||||
| @ -140,17 +140,17 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Add log in Moodle.
 |             // Add log in Moodle.
 | ||||||
|             CoreCourse.logView( |             CoreUtils.ignoreErrors(CoreCourse.logView( | ||||||
|                 this.siteHomeId, |                 this.siteHomeId, | ||||||
|                 undefined, |                 undefined, | ||||||
|                 undefined, |                 undefined, | ||||||
|                 this.currentSite.getInfo()?.sitename, |                 this.currentSite.getInfo()?.sitename, | ||||||
|             ).catch(() => { |             )); | ||||||
|                 // Ignore errors.
 |  | ||||||
|             }); |  | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             CoreDomUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true); |             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; |             return; | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|  |         promises.push(CoreCourse.invalidateCourseBlocks(this.siteHomeId)); | ||||||
|  | 
 | ||||||
|         if (this.section && this.section.modules) { |         if (this.section && this.section.modules) { | ||||||
|             // Invalidate modules prefetch data.
 |             // Invalidate modules prefetch data.
 | ||||||
|             promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId)); |             promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (this.courseBlocksComponent) { |  | ||||||
|             promises.push(this.courseBlocksComponent.invalidateBlocks()); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Promise.all(promises).finally(async () => { |         Promise.all(promises).finally(async () => { | ||||||
|             const p2: Promise<unknown>[] = []; |             await this.loadContent().finally(() => { | ||||||
| 
 |  | ||||||
|             p2.push(this.loadContent()); |  | ||||||
|             if (this.courseBlocksComponent) { |  | ||||||
|                 p2.push(this.courseBlocksComponent.loadContent()); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             await Promise.all(p2).finally(() => { |  | ||||||
|                 refresher?.complete(); |                 refresher?.complete(); | ||||||
|             }); |             }); | ||||||
|         }); |         }); | ||||||
|  | |||||||
| @ -29,7 +29,15 @@ import { CoreIonLoadingElement } from '@classes/ion-loading'; | |||||||
| import { CoreCanceledError } from '@classes/errors/cancelederror'; | import { CoreCanceledError } from '@classes/errors/cancelederror'; | ||||||
| import { CoreAnyError, CoreError } from '@classes/errors/error'; | import { CoreAnyError, CoreError } from '@classes/errors/error'; | ||||||
| import { CoreSilentError } from '@classes/errors/silenterror'; | import { CoreSilentError } from '@classes/errors/silenterror'; | ||||||
| import { makeSingleton, Translate, AlertController, ToastController, PopoverController, ModalController } from '@singletons'; | import { | ||||||
|  |     makeSingleton, | ||||||
|  |     Translate, | ||||||
|  |     AlertController, | ||||||
|  |     ToastController, | ||||||
|  |     PopoverController, | ||||||
|  |     ModalController, | ||||||
|  |     Router, | ||||||
|  | } from '@singletons'; | ||||||
| import { CoreLogger } from '@singletons/logger'; | import { CoreLogger } from '@singletons/logger'; | ||||||
| import { CoreFileSizeSum } from '@services/plugin-file-delegate'; | import { CoreFileSizeSum } from '@services/plugin-file-delegate'; | ||||||
| import { CoreNetworkError } from '@classes/errors/network-error'; | import { CoreNetworkError } from '@classes/errors/network-error'; | ||||||
| @ -41,6 +49,9 @@ import { CoreZoomLevel } from '@features/settings/services/settings-helper'; | |||||||
| import { CoreErrorWithTitle } from '@classes/errors/errorwithtitle'; | import { CoreErrorWithTitle } from '@classes/errors/errorwithtitle'; | ||||||
| import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/handlers/multilang'; | import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/handlers/multilang'; | ||||||
| import { CoreSites } from '@services/sites'; | import { CoreSites } from '@services/sites'; | ||||||
|  | import { NavigationStart } from '@angular/router'; | ||||||
|  | import { filter } from 'rxjs/operators'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
|  * "Utils" service with helper functions for UI, DOM elements and HTML code. |  * "Utils" service with helper functions for UI, DOM elements and HTML code. | ||||||
| @ -1651,18 +1662,32 @@ export class CoreDomUtilsProvider { | |||||||
|     /** |     /** | ||||||
|      * Opens a Modal. |      * Opens a Modal. | ||||||
|      * |      * | ||||||
|      * @param modalOptions Modal Options. |      * @param options Modal Options. | ||||||
|      */ |      */ | ||||||
|     async openModal<T = unknown>( |     async openModal<T = unknown>( | ||||||
|         modalOptions: ModalOptions, |         options: OpenModalOptions, | ||||||
|     ): Promise<T | undefined> { |     ): Promise<T | undefined> { | ||||||
| 
 | 
 | ||||||
|  |         const { waitForDismissCompleted, closeOnNavigate, ...modalOptions } = options; | ||||||
|  |         const listenCloseEvents = closeOnNavigate ?? true; // Default to true.
 | ||||||
|  | 
 | ||||||
|         const modal = await ModalController.create(modalOptions); |         const modal = await ModalController.create(modalOptions); | ||||||
| 
 | 
 | ||||||
|  |         let navSubscription: Subscription | undefined; | ||||||
|  |         if (listenCloseEvents) { | ||||||
|  |             // Listen navigation events to close modals.
 | ||||||
|  |             navSubscription = Router.events | ||||||
|  |                 .pipe(filter(event => event instanceof NavigationStart)) | ||||||
|  |                 .subscribe(async () => { | ||||||
|  |                     modal.dismiss(); | ||||||
|  |                 }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         await modal.present(); |         await modal.present(); | ||||||
| 
 | 
 | ||||||
|         // If onDidDismiss is nedded we can add a new param to the function to wait one function or the other.
 |         const result = waitForDismissCompleted ? await modal.onDidDismiss<T>() : await modal.onWillDismiss<T>(); | ||||||
|         const result = await modal.onWillDismiss<T>(); |         navSubscription?.unsubscribe(); | ||||||
|  | 
 | ||||||
|         if (result?.data) { |         if (result?.data) { | ||||||
|             return result?.data; |             return result?.data; | ||||||
|         } |         } | ||||||
| @ -1671,21 +1696,21 @@ export class CoreDomUtilsProvider { | |||||||
|     /** |     /** | ||||||
|      * Opens a side Modal. |      * Opens a side Modal. | ||||||
|      * |      * | ||||||
|      * @param modalOptions Modal Options. |      * @param options Modal Options. | ||||||
|      */ |      */ | ||||||
|     async openSideModal<T = unknown>( |     async openSideModal<T = unknown>( | ||||||
|         modalOptions: ModalOptions, |         options: OpenModalOptions, | ||||||
|     ): Promise<T | undefined> { |     ): Promise<T | undefined> { | ||||||
| 
 | 
 | ||||||
|         modalOptions = Object.assign({ |         options = Object.assign({ | ||||||
|             cssClass: 'core-modal-lateral', |             cssClass: 'core-modal-lateral', | ||||||
|             showBackdrop: true, |             showBackdrop: true, | ||||||
|             backdropDismiss: true, |             backdropDismiss: true, | ||||||
|             enterAnimation: CoreModalLateralTransitionEnter, |             enterAnimation: CoreModalLateralTransitionEnter, | ||||||
|             leaveAnimation: CoreModalLateralTransitionLeave, |             leaveAnimation: CoreModalLateralTransitionLeave, | ||||||
|         }, modalOptions); |         }, options); | ||||||
| 
 | 
 | ||||||
|         return await this.openModal<T>(modalOptions); |         return await this.openModal<T>(options); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -2012,3 +2037,11 @@ export const CoreDomUtils = makeSingleton(CoreDomUtilsProvider); | |||||||
| export type OpenPopoverOptions = PopoverOptions & { | export type OpenPopoverOptions = PopoverOptions & { | ||||||
|     waitForDismissCompleted?: boolean; |     waitForDismissCompleted?: boolean; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Options for the openModal function. | ||||||
|  |  */ | ||||||
|  | export type OpenModalOptions = ModalOptions & { | ||||||
|  |     waitForDismissCompleted?: boolean; | ||||||
|  |     closeOnNavigate?: boolean; // Default true.
 | ||||||
|  | }; | ||||||
|  | |||||||
| @ -197,13 +197,6 @@ | |||||||
|         --background: var(--core-progressbar-background); |         --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-background:        #{$ion-item-background}; | ||||||
|     --ion-item-detail-icon-color: var(--gray-darker); |     --ion-item-detail-icon-color: var(--gray-darker); | ||||||
|     --ion-item-detail-icon-font-size: 20px; |     --ion-item-detail-icon-font-size: 20px; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user