diff --git a/scripts/langindex.json b/scripts/langindex.json index 1df9a198e..94f7fe43e 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -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", diff --git a/src/core/components/empty-box/empty-box.scss b/src/core/components/empty-box/empty-box.scss index 99ffdc978..b0b3c2254 100644 --- a/src/core/components/empty-box/empty-box.scss +++ b/src/core/components/empty-box/empty-box.scss @@ -72,7 +72,3 @@ height: auto; } } - -:host-context(core-block-course-blocks) .core-empty-box { - position: relative; -} diff --git a/src/core/features/block/components/components.module.ts b/src/core/features/block/components/components.module.ts index 7d64e292d..ab8bf883c 100644 --- a/src/core/features/block/components/components.module.ts +++ b/src/core/features/block/components/components.module.ts @@ -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 {} diff --git a/src/core/features/block/components/course-blocks/core-block-course-blocks.html b/src/core/features/block/components/course-blocks/core-block-course-blocks.html deleted file mode 100644 index 120e60749..000000000 --- a/src/core/features/block/components/course-blocks/core-block-course-blocks.html +++ /dev/null @@ -1,15 +0,0 @@ -
- -
- -
- - - - - - - - -
diff --git a/src/core/features/block/components/course-blocks/course-blocks.scss b/src/core/features/block/components/course-blocks/course-blocks.scss deleted file mode 100644 index c7ba5b65e..000000000 --- a/src/core/features/block/components/course-blocks/course-blocks.scss +++ /dev/null @@ -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); - } - } -} diff --git a/src/core/features/block/components/side-blocks-button/side-blocks-button.html b/src/core/features/block/components/side-blocks-button/side-blocks-button.html new file mode 100644 index 000000000..b4d634937 --- /dev/null +++ b/src/core/features/block/components/side-blocks-button/side-blocks-button.html @@ -0,0 +1,3 @@ + + + diff --git a/src/core/features/block/components/side-blocks-button/side-blocks-button.scss b/src/core/features/block/components/side-blocks-button/side-blocks-button.scss new file mode 100644 index 000000000..3e9277bfc --- /dev/null +++ b/src/core/features/block/components/side-blocks-button/side-blocks-button.scss @@ -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; + } +} diff --git a/src/core/features/block/components/side-blocks-button/side-blocks-button.ts b/src/core/features/block/components/side-blocks-button/side-blocks-button.ts new file mode 100644 index 000000000..71f3e4898 --- /dev/null +++ b/src/core/features/block/components/side-blocks-button/side-blocks-button.ts @@ -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, + }, + }); + } + +} diff --git a/src/core/features/block/components/side-blocks/side-blocks.html b/src/core/features/block/components/side-blocks/side-blocks.html new file mode 100644 index 000000000..627139e3c --- /dev/null +++ b/src/core/features/block/components/side-blocks/side-blocks.html @@ -0,0 +1,26 @@ + + +

{{ 'core.block.blocks' | translate }}

+ + + + + +
+
+ + + + + + + + + + + + + + + diff --git a/src/core/features/block/components/course-blocks/course-blocks.ts b/src/core/features/block/components/side-blocks/side-blocks.ts similarity index 61% rename from src/core/features/block/components/course-blocks/course-blocks.ts rename to src/core/features/block/components/side-blocks/side-blocks.ts index ad8b01d5f..18dc3c9ca 100644 --- a/src/core/features/block/components/course-blocks/course-blocks.ts +++ b/src/core/features/block/components/side-blocks/side-blocks.ts @@ -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; - 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 { - 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 { - 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 { + async doRefresh(refresher?: IonRefresher): Promise { await CoreUtils.ignoreErrors(this.invalidateBlocks()); - await this.loadContent(); + await this.loadContent().finally(() => { + refresher?.complete(); + }); + } + + /** + * Close modal. + */ + closeModal(): void { + ModalController.dismiss(); } } diff --git a/src/core/features/block/lang.json b/src/core/features/block/lang.json index 9b136b8ee..cc3f3c95a 100644 --- a/src/core/features/block/lang.json +++ b/src/core/features/block/lang.json @@ -1,3 +1,5 @@ { - "blocks": "Blocks" -} \ No newline at end of file + "blocks": "Blocks", + "noblocks": "No blocks found!", + "opendrawerblocks": "Open block drawer" +} diff --git a/src/core/features/block/services/block-helper.ts b/src/core/features/block/services/block-helper.ts index c946bd75b..00b33cd23 100644 --- a/src/core/features/block/services/block-helper.ts +++ b/src/core/features/block/services/block-helper.ts @@ -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 { + try { + const blocks = await this.getCourseBlocks(courseId); + + return blocks.length > 0; + } catch { + return false; + } + } + } export const CoreBlockHelper = makeSingleton(CoreBlockHelperProvider); diff --git a/src/core/features/course/components/format/core-course-format.html b/src/core/features/course/components/format/core-course-format.html index def9a3009..e4b211c67 100644 --- a/src/core/features/course/components/format/core-course-format.html +++ b/src/core/features/course/components/format/core-course-format.html @@ -6,126 +6,115 @@ + + + + + - +
+ + + + + {{ 'core.course.sections' | translate }} + + + + +
+
- - - - - - -
- - - - - {{ 'core.course.sections' | translate }} - - - - + + + +
+
+ + + + + + + + {{ 'core.course.hiddenfromstudents' | translate }} + + + {{ 'core.notavailable' | translate }} + + + + + + + +
+
+ + +
+ + + + + +
+ + +
+ + + + + + - - - -
- -
- - - - - - - - {{ 'core.course.hiddenfromstudents' | translate }} - - - {{ 'core.notavailable' | translate }} - - - - - - - -
-
+ +
- -
- - - - - -
+ + + + + + + + + + + + - -
- - - - - - - - - -
- - - - - - - - - - - - - - - - + + + +
+ [class.core-section-download]="downloadEnabled" [class.item-dimmed]="section.visible === 0 || section.uservisible === false">

diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/format/format.ts index a76e10358..6e8ae36bc 100644 --- a/src/core/features/course/components/format/format.ts +++ b/src/core/features/course/components/format/format.ts @@ -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(); // Notify when any module completion changes. @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList; - @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent; // All the possible component classes. courseFormatComponent?: Type; @@ -92,8 +90,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { showSectionId = 0; data: Record = {}; // 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 { 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); diff --git a/src/core/features/sitehome/pages/index/index.html b/src/core/features/sitehome/pages/index/index.html index f96e2ea06..9220391ae 100644 --- a/src/core/features/sitehome/pages/index/index.html +++ b/src/core/features/sitehome/pages/index/index.html @@ -3,11 +3,9 @@ - - + @@ -15,49 +13,52 @@ - - - - - - - - - + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - + + + + + - - - + + + + @@ -88,13 +89,17 @@ -

{{ 'core.courses.mycourses' | translate}}

+ +

{{ 'core.courses.mycourses' | translate}}

+
-

{{ 'core.courses.searchcourses' | translate}}

+ +

{{ 'core.courses.searchcourses' | translate}}

+
diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index 777d6ce9c..eae6406f8 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -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[] = []; - - p2.push(this.loadContent()); - if (this.courseBlocksComponent) { - p2.push(this.courseBlocksComponent.loadContent()); - } - - await Promise.all(p2).finally(() => { + await this.loadContent().finally(() => { refresher?.complete(); }); }); diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index 56998e1eb..fa001efb9 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -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;