MOBILE-3608 blocks: Migrate basic block structure

main
Pau Ferrer Ocaña 2020-12-07 17:04:31 +01:00
parent 3356659a24
commit 7d1d318afc
26 changed files with 1103 additions and 10 deletions

View File

@ -2,6 +2,10 @@
margin: 0;
}
:host-context([dir=rtl]).icon-flip-rtl {
transform: scaleX(-1);
}
:host-context(ion-item.md) ion-icon {
&[slot] {
font-size: 1.6em;

View File

@ -0,0 +1,24 @@
// (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 { NgModule } from '@angular/core';
import { CoreBlockDefaultHandler } from './services/handlers/default-block';
@NgModule({
providers: [
CoreBlockDefaultHandler,
],
})
export class CoreBlockModule {
}

View File

@ -0,0 +1,143 @@
// (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 { OnInit, Input, Component, Optional, Inject } from '@angular/core';
import { CoreLogger } from '@singletons/logger';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreTextUtils } from '@services/utils/text';
import { CoreCourseBlock } from '../../course/services/course';
import { IonRefresher } from '@ionic/angular';
import { Params } from '@angular/router';
/**
* Template class to easily create components for blocks.
*/
@Component({
template: '',
})
export abstract class CoreBlockBaseComponent implements OnInit {
@Input() title!: string; // The block title.
@Input() block!: CoreCourseBlock; // The block to render.
@Input() contextLevel!: string; // The context where the block will be used.
@Input() instanceId!: number; // The instance ID associated with the context level.
@Input() link?: string; // Link to go when clicked.
@Input() linkParams?: Params; // Link params to go when clicked.
loaded = false; // If the component has been loaded.
protected fetchContentDefaultError = ''; // Default error to show when loading contents.
protected logger: CoreLogger;
constructor(@Optional() @Inject('') loggerName: string = 'AddonBlockComponent') {
this.logger = CoreLogger.getInstance(loggerName);
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
if (this.block.configs && this.block.configs.length > 0) {
this.block.configs.map((config) => {
config.value = CoreTextUtils.instance.parseJSON(config.value);
return config;
});
this.block.configsRecord = CoreUtils.instance.arrayToObject(this.block.configs, 'name');
}
await this.loadContent();
}
/**
* Refresh the data.
*
* @param refresher Refresher.
* @param done Function to call when done.
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, showErrors: boolean = false): Promise<void> {
if (this.loaded) {
return this.refreshContent(showErrors).finally(() => {
refresher?.detail.complete();
done && done();
});
}
}
/**
* Perform the refresh content function.
*
* @param showErrors Wether to show errors to the user or hide them.
* @return Resolved when done.
*/
protected async refreshContent(showErrors: boolean = false): Promise<void> {
// Wrap the call in a try/catch so the workflow isn't interrupted if an error occurs.
try {
await this.invalidateContent();
} catch (ex) {
// An error ocurred in the function, log the error and just resolve the promise so the workflow continues.
this.logger.error(ex);
}
await this.loadContent(true, showErrors);
}
/**
* Perform the invalidate content function.
*
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
return;
}
/**
* Loads the component contents and shows the corresponding error.
*
* @param refresh Whether we're refreshing data.
* @param showErrors Wether to show errors to the user or hide them.
* @return Promise resolved when done.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async loadContent(refresh?: boolean, showErrors: boolean = false): Promise<void> {
// Wrap the call in a try/catch so the workflow isn't interrupted if an error occurs.
try {
await this.fetchContent(refresh);
} catch (error) {
// An error ocurred in the function, log the error and just resolve the promise so the workflow continues.
this.logger.error(error);
// Error getting data, fail.
CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError, true);
}
this.loaded = true;
}
/**
* Download the component contents.
*
* @param refresh Whether we're refreshing data.
* @return Promise resolved when done.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async fetchContent(refresh: boolean = false): Promise<void> {
return;
}
}

View File

@ -0,0 +1,61 @@
// (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 { CoreCourseBlock } from '@features/course/services/course';
import { CoreBlockPreRenderedComponent } from '../components/pre-rendered-block/pre-rendered-block';
import { CoreBlockHandler, CoreBlockHandlerData } from '../services/block-delegate';
/**
* Base handler for blocks.
*
* This class is needed because parent classes cannot have @Injectable in Angular v6, so the default handler cannot be a
* parent class.
*/
export class CoreBlockBaseHandler implements CoreBlockHandler {
name = 'CoreBlockBase';
blockName = 'base';
/**
* Whether or not the handler is enabled on a site level.
*
* @return True or promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* Returns the data needed to render the block.
*
* @param block The block to render.
* @param contextLevel The context where the block will be used.
* @param instanceId The instance ID associated with the context level.
* @return Data or promise resolved with the data.
*/
getDisplayData(
block: CoreCourseBlock, // eslint-disable-line @typescript-eslint/no-unused-vars
contextLevel: string, // eslint-disable-line @typescript-eslint/no-unused-vars
instanceId: number, // eslint-disable-line @typescript-eslint/no-unused-vars
): CoreBlockHandlerData | Promise<CoreBlockHandlerData> {
// To be overridden.
return {
title: '',
class: '',
component: CoreBlockPreRenderedComponent,
};
}
}

View File

@ -0,0 +1,30 @@
:host {
// @todo
position: relative;
display: block;
core-loading.core-loading-center {
display: block;
.core-loading-container {
margin-top: 10px;
position: relative;
}
}
core-empty-box .core-empty-box {
position: relative;
z-index: initial;
//@include position(initial, initial, null, initial);
height: auto;
}
ion-item-divider {
//@include padding-horizontal(null, 0px);
min-height: 60px;
}
ion-item-divider .core-button-spinner {
margin: 0;
}
}

View File

@ -0,0 +1,163 @@
// (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, OnInit, ViewChild, OnDestroy, DoCheck, KeyValueDiffers, KeyValueDiffer, Type } from '@angular/core';
import { CoreBlockDelegate } from '../../services/block-delegate';
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
import { Subscription } from 'rxjs';
import { CoreCourseBlock } from '@/core/features/course/services/course';
import { IonRefresher } from '@ionic/angular';
/**
* Component to render a block.
*/
@Component({
selector: 'core-block',
templateUrl: 'core-block.html',
styleUrls: ['block.scss'],
})
export class CoreBlockComponent implements OnInit, OnDestroy, DoCheck {
@ViewChild(CoreDynamicComponent) dynamicComponent?: CoreDynamicComponent;
@Input() block!: CoreCourseBlock; // The block to render.
@Input() contextLevel!: string; // The context where the block will be used.
@Input() instanceId!: number; // The instance ID associated with the context level.
@Input() extraData: any; // Any extra data to be passed to the block.
componentClass?: Type<unknown>; // The class of the component to render.
data: any = {}; // Data to pass to the component.
class?: string; // CSS class to apply to the block.
loaded = false;
blockSubscription?: Subscription;
protected differ: KeyValueDiffer<unknown, unknown>; // To detect changes in the data input.
constructor(
differs: KeyValueDiffers,
) {
this.differ = differs.find([]).create();
}
/**
* Component being initialized.
*/
ngOnInit(): void {
if (!this.block) {
this.loaded = true;
return;
}
if (this.block.visible) {
// Get the data to render the block.
this.initBlock();
}
}
/**
* Detect and act upon changes that Angular cant or wont detect on its own (objects and arrays).
*/
ngDoCheck(): void {
if (this.data) {
// Check if there's any change in the extraData object.
const changes = this.differ.diff(this.extraData);
if (changes) {
this.data = Object.assign(this.data, this.extraData || {});
}
}
}
/**
* Get block display data and initialises the block once this is available. If the block is not
* supported at the moment, try again if the available blocks are updated (because it comes
* from a site plugin).
*/
async initBlock(): Promise<void> {
try {
const data = await CoreBlockDelegate.instance.getBlockDisplayData(this.block, this.contextLevel, this.instanceId);
if (!data) {
// Block not supported, don't render it. But, site plugins might not have finished loading.
// Subscribe to the observable in block delegate that will tell us if blocks are updated.
// We can retry init later if that happens.
this.blockSubscription = CoreBlockDelegate.instance.blocksUpdateObservable.subscribe(
(): void => {
this.blockSubscription?.unsubscribe();
delete this.blockSubscription;
this.initBlock();
},
);
return;
}
this.class = data.class;
this.componentClass = data.component;
// Set up the data needed by the block component.
this.data = Object.assign({
title: data.title,
block: this.block,
contextLevel: this.contextLevel,
instanceId: this.instanceId,
link: data.link || null,
linkParams: data.linkParams || null,
}, this.extraData || {}, data.componentData || {});
} catch {
// Ignore errors.
}
this.loaded = true;
}
/**
* On destroy of the component, clear up any subscriptions.
*/
ngOnDestroy(): void {
this.blockSubscription?.unsubscribe();
delete this.blockSubscription;
}
/**
* Refresh the data.
*
* @param refresher Refresher. Please pass this only if the refresher should finish when this function finishes.
* @param done Function to call when done.
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
async doRefresh(
refresher?: CustomEvent<IonRefresher>,
done?: () => void,
showErrors: boolean = false,
): Promise<void> {
if (this.dynamicComponent) {
await this.dynamicComponent.callComponentFunction('doRefresh', [refresher, done, showErrors]);
}
}
/**
* Invalidate some data.
*
* @return Promise resolved when done.
*/
async invalidate(): Promise<void> {
if (this.dynamicComponent) {
await this.dynamicComponent.callComponentFunction('invalidateContent');
}
}
}

View File

@ -0,0 +1,4 @@
<!-- Only render the block if it's supported. -->
<div *ngIf="loaded && componentClass && block.visible" class="{{class}}">
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
</div>

View File

@ -0,0 +1,52 @@
// (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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
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 { CoreComponentsModule } from '@components/components.module';
@NgModule({
declarations: [
CoreBlockComponent,
CoreBlockOnlyTitleComponent,
CoreBlockPreRenderedComponent,
CoreBlockCourseBlocksComponent,
],
imports: [
CommonModule,
IonicModule,
CoreDirectivesModule,
TranslateModule.forChild(),
CoreComponentsModule,
],
exports: [
CoreBlockComponent,
CoreBlockOnlyTitleComponent,
CoreBlockPreRenderedComponent,
CoreBlockCourseBlocksComponent,
],
entryComponents: [
CoreBlockOnlyTitleComponent,
CoreBlockPreRenderedComponent,
CoreBlockCourseBlocksComponent,
],
})
export class CoreBlockComponentsModule {}

View File

@ -0,0 +1,14 @@
<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" class="core-loading-center">
<ion-list>
<!-- Course expand="block"s. -->
<ng-container *ngFor="let block of blocks">
<core-block *ngIf="block.visible" [block]="block" contextLevel="course" [instanceId]="courseId" [extraData]="{'downloadEnabled': downloadEnabled}"></core-block>
</ng-container>
</ion-list>
</core-loading>
</div>

View File

@ -0,0 +1,58 @@
:host {
&.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%;
// @todo @include core-split-area-start();
}
div.core-course-blocks-side {
max-width: var(--side-blocks-max-width);
min-width: var(--side-blocks-min-width);
box-shadow: -4px 0px 16px rgba(0, 0, 0, 0.18);
// @todo @include core-split-area-end();
}
.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: 4px 0px 16px rgba(0, 0, 0, 0.18);
}
}
}

View File

@ -0,0 +1,111 @@
// (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, ViewChildren, Input, OnInit, QueryList, ElementRef } from '@angular/core';
import { IonContent } from '@ionic/angular';
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';
/**
* Component that displays the list of course blocks.
*/
@Component({
selector: 'core-block-course-blocks',
templateUrl: 'core-block-course-blocks.html',
styleUrls: ['course-blocks.scss'],
})
export class CoreBlockCourseBlocksComponent implements OnInit {
@Input() courseId!: number;
@Input() hideBlocks = false;
@Input() hideBottomBlocks = false;
@Input() downloadEnabled = false;
@ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>;
dataLoaded = false;
blocks: CoreCourseBlock[] = [];
protected element: HTMLElement;
constructor(
element: ElementRef,
protected content: IonContent,
) {
this.element = element.nativeElement;
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
this.element.classList.add('core-no-blocks');
this.loadContent().finally(() => {
this.dataLoaded = true;
});
}
/**
* Invalidate blocks data.
*
* @return Promise resolved when done.
*/
async invalidateBlocks(): Promise<void> {
const promises: Promise<void>[] = [];
if (CoreBlockHelper.instance.canGetCourseBlocks()) {
promises.push(CoreCourse.instance.invalidateCourseBlocks(this.courseId));
}
// Invalidate the blocks.
this.blocksComponents?.forEach((blockComponent) => {
promises.push(blockComponent.invalidate().catch(() => {
// Ignore errors.
}));
});
await Promise.all(promises);
}
/**
* Convenience function to fetch the data.
*
* @return Promise resolved when done.
*/
async loadContent(): Promise<void> {
try {
this.blocks = await CoreBlockHelper.instance.getCourseBlocks(this.courseId);
} catch (error) {
CoreDomUtils.instance.showErrorModal(error);
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');
}
}
}

View File

@ -0,0 +1,4 @@
<ion-item-divider class="ion-text-wrap" (click)="gotoBlock()">
<ion-label><h2>{{ title | translate }}</h2></ion-label>
<ion-icon class="item-detail-icon" name="chevron-forward-outline" slot="end" flip-rtl></ion-icon>
</ion-item-divider>

View File

@ -0,0 +1,5 @@
:host {
ion-item-divider {
cursor: pointer;
}
}

View File

@ -0,0 +1,49 @@
// (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 { OnInit, Component } from '@angular/core';
import { CoreBlockBaseComponent } from '../../classes/base-block-component';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Component to render blocks with only a title and link.
*/
@Component({
selector: 'core-block-only-title',
templateUrl: 'core-block-only-title.html',
styleUrls: ['only-title-block.scss'],
})
export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implements OnInit {
constructor() {
super('CoreBlockOnlyTitleComponent');
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
await super.ngOnInit();
this.fetchContentDefaultError = 'Error getting ' + this.block.contents?.title + ' data.';
}
/**
* Go to the block page.
*/
gotoBlock(): void {
CoreNavHelper.instance.goInSite(this.link!, this.linkParams!, undefined, true);
}
}

View File

@ -0,0 +1,25 @@
<ion-item-divider class="ion-text-wrap" *ngIf="title" sticky="true">
<ion-label>
<h2>
<core-format-text [text]="title | translate" contextLevel="block" [contextInstanceId]="block.instanceid"
[courseId]="courseId">
</core-format-text>
</h2>
</ion-label>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<ion-item *ngIf="block.contents?.content" class="ion-text-wrap core-block-content">
<ion-label>
<core-format-text [text]="block.contents?.content" contextLevel="block" [contextInstanceId]="block.instanceid"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<ion-item *ngIf="block.contents?.footer" class="ion-text-wrap core-block-footer">
<ion-label>
<core-format-text [text]="block.contents?.footer" contextLevel="block" [contextInstanceId]="block.instanceid"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</core-loading>

View File

@ -0,0 +1,44 @@
// (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 { OnInit, Component } from '@angular/core';
import { CoreBlockBaseComponent } from '../../classes/base-block-component';
/**
* Component to render blocks with pre-rendered HTML.
*/
@Component({
selector: 'core-block-pre-rendered',
templateUrl: 'core-block-pre-rendered.html',
})
export class CoreBlockPreRenderedComponent extends CoreBlockBaseComponent implements OnInit {
courseId?: number;
constructor() {
super('CoreBlockPreRenderedComponent');
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
await super.ngOnInit();
this.courseId = this.contextLevel == 'course' ? this.instanceId : undefined;
this.fetchContentDefaultError = 'Error getting ' + this.block.contents?.title + ' data.';
}
}

View File

@ -0,0 +1,3 @@
{
"blocks": "Blocks"
}

View File

@ -0,0 +1,203 @@
// (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 { Injectable, Type } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { CoreSite } from '@classes/site';
import { Subject } from 'rxjs';
import { CoreCourseBlock } from '@features/course/services/course';
import { Params } from '@angular/router';
import { makeSingleton } from '@singletons';
/**
* Interface that all blocks must implement.
*/
export interface CoreBlockHandler extends CoreDelegateHandler {
/**
* Name of the block the handler supports. E.g. 'activity_modules'.
*/
blockName: string;
/**
* Returns the data needed to render the block.
*
* @param block The block to render.
* @param contextLevel The context where the block will be used.
* @param instanceId The instance ID associated with the context level.
* @return Data or promise resolved with the data.
*/
getDisplayData?(
block: CoreCourseBlock,
contextLevel: string,
instanceId: number,
): CoreBlockHandlerData | Promise<CoreBlockHandlerData>;
}
/**
* Data needed to render a block. It's returned by the handler.
*/
export interface CoreBlockHandlerData {
/**
* Title to display for the block.
*/
title: string;
/**
* Class to add to the displayed block.
*/
class?: string;
/**
* The component to render the contents of the block.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*/
component: Type<unknown>;
/**
* Data to pass to the component. All the properties in this object will be passed to the component as inputs.
*/
componentData?: Record<string | number, unknown>;
/**
* Link to go when showing only title.
*/
link?: string;
/**
* Params of the link.
*/
linkParams?: Params;
}
/**
* Delegate to register block handlers.
*/
@Injectable({ providedIn: 'root' })
export class CoreBlockDelegateService extends CoreDelegate<CoreBlockHandler> {
protected handlerNameProperty = 'blockName';
protected featurePrefix = 'CoreBlockDelegate_';
blocksUpdateObservable: Subject<void>;
constructor() {
super('CoreBlockDelegate', true);
this.blocksUpdateObservable = new Subject<void>();
}
/**
* Check if blocks are disabled in a certain site.
*
* @param site Site. If not defined, use current site.
* @return Whether it's disabled.
*/
areBlocksDisabledInSite(site?: CoreSite): boolean {
site = site || CoreSites.instance.getCurrentSite();
return !!site && site.isFeatureDisabled('NoDelegate_SiteBlocks');
}
/**
* Check if blocks are disabled in a certain site for courses.
*
* @param site Site. If not defined, use current site.
* @return Whether it's disabled.
*/
areBlocksDisabledInCourses(site?: CoreSite): boolean {
site = site || CoreSites.instance.getCurrentSite();
return !!site && site.isFeatureDisabled('NoDelegate_CourseBlocks');
}
/**
* Check if blocks are disabled in a certain site.
*
* @param siteId Site Id. If not defined, use current site.
* @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
*/
async areBlocksDisabled(siteId?: string): Promise<boolean> {
const site = await CoreSites.instance.getSite(siteId);
return this.areBlocksDisabledInSite(site);
}
/**
* Get the display data for a certain block.
*
* @param injector Injector.
* @param block The block to render.
* @param contextLevel The context where the block will be used.
* @param instanceId The instance ID associated with the context level.
* @return Promise resolved with the display data.
*/
async getBlockDisplayData(
block: CoreCourseBlock,
contextLevel: string,
instanceId: number,
): Promise<CoreBlockHandlerData | undefined> {
return this.executeFunctionOnEnabled(
block.name,
'getDisplayData',
[block, contextLevel, instanceId],
);
}
/**
* Check if any of the blocks in a list is supported.
*
* @param blocks The list of blocks.
* @return Whether any of the blocks is supported.
*/
hasSupportedBlock(blocks: CoreCourseBlock[]): boolean {
blocks = blocks || [];
return !!blocks.find((block) => this.isBlockSupported(block.name));
}
/**
* Check if a block is supported.
*
* @param name Block "name". E.g. 'activity_modules'.
* @return Whether it's supported.
*/
isBlockSupported(name: string): boolean {
return this.hasHandler(name, true);
}
/**
* Check if feature is enabled or disabled in the site, depending on the feature prefix and the handler name.
*
* @param handler Handler to check.
* @param site Site to check.
* @return Whether is enabled or disabled in site.
*/
protected isFeatureDisabled(handler: CoreBlockHandler, site: CoreSite): boolean {
return this.areBlocksDisabledInSite(site) || super.isFeatureDisabled(handler, site);
}
/**
* Called when there are new block handlers available. Informs anyone who subscribed to the
* observable.
*/
updateData(): void {
this.blocksUpdateObservable.next();
}
}
export class CoreBlockDelegate extends makeSingleton(CoreBlockDelegateService) {}

View File

@ -0,0 +1,60 @@
// (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 { Injectable } from '@angular/core';
import { CoreCourse, CoreCourseBlock } from '@features/course/services/course';
import { CoreBlockDelegate } from './block-delegate';
import { makeSingleton } from '@singletons';
/**
* Service that provides helper functions for blocks.
*/
@Injectable({ providedIn: 'root' })
export class CoreBlockHelperProvider {
/**
* Return if it get course blocks options is enabled for the current site.
*
* @return true if enabled, false otherwise.
*/
canGetCourseBlocks(): boolean {
return CoreCourse.instance.canGetCourseBlocks() && !CoreBlockDelegate.instance.areBlocksDisabledInCourses();
}
/**
* Returns the list of blocks for the selected course.
*
* @param courseId Course ID.
* @return List of supported blocks.
*/
async getCourseBlocks(courseId: number): Promise<CoreCourseBlock[]> {
const canGetBlocks = this.canGetCourseBlocks();
if (!canGetBlocks) {
return [];
}
const blocks = await CoreCourse.instance.getCourseBlocks(courseId);
const hasSupportedBlock = CoreBlockDelegate.instance.hasSupportedBlock(blocks);
if (!hasSupportedBlock) {
return [];
}
return blocks;
}
}
export class CoreBlockHelper extends makeSingleton(CoreBlockHelperProvider) {}

View File

@ -0,0 +1,27 @@
// (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 { Injectable } from '@angular/core';
import { CoreBlockBaseHandler } from '../../classes/base-block-handler';
/**
* Default handler used when a block type doesn't have a specific implementation.
*/
@Injectable()
export class CoreBlockDefaultHandler extends CoreBlockBaseHandler {
name = 'CoreBlockDefault';
blockName = 'default';
}

View File

@ -31,8 +31,6 @@ export class CoreContentLinksModuleListHandler extends CoreContentLinksHandlerBa
/**
* Construct the handler.
*
* @param linkHelper The CoreContentLinksHelperProvider instance.
* @param translate The TranslateService instance.
* @param addon Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled.
* @param modName Name of the module (assign, book, ...).
*/

View File

@ -1301,6 +1301,11 @@ export type CoreCourseBlock = {
value: string; // JSON encoded representation of the config value.
type: string; // Type (instance or plugin).
}[];
configsRecord?: Record<string, { // Block instance and plugin configuration settings.
name: string; // Name.
value: string; // JSON encoded representation of the config value.
type: string; // Type (instance or plugin).
}>;
};
/**

View File

@ -16,7 +16,7 @@
(ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<!-- @todo <core-block-course-blocks [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled">-->
<core-block-course-blocks [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled">
<core-loading [hideUntil]="dataLoaded">
<ion-list>
<!-- Site home main contents. -->
@ -56,7 +56,7 @@
</core-empty-box>
</core-loading>
<!-- @todo </core-block-course-blocks> -->
</core-block-course-blocks>
</ion-content>
<ng-template #allCourseList>

View File

@ -20,6 +20,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreComponentsModule } from '@components/components.module';
import { CoreBlockComponentsModule } from '@/core/features/block/components/components.module';
import { CoreSiteHomeIndexPage } from '.';
@ -38,6 +39,7 @@ const routes: Routes = [
TranslateModule.forChild(),
CoreDirectivesModule,
CoreComponentsModule,
CoreBlockComponentsModule,
],
declarations: [
CoreSiteHomeIndexPage,

View File

@ -40,11 +40,6 @@ ion-icon {
}
}
[dir=rtl] ion-icon.icon-flip-rtl {
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1);
}
// Ionic alert.
ion-alert.core-alert-network-error .alert-head {
position: relative;
@ -77,7 +72,11 @@ ion-alert.core-nohead {
// Ionic item divider.
ion-item-divider {
--background: var(--gray-lighter);
border: 0;
.item-detail-icon {
font-size: 20px;
opacity: 0.25;
padding-inline-end: 16px;
}
}
// Ionic list.

View File

@ -151,6 +151,11 @@
--background: var(--custom-progress-background, var(--gray-lighter));
}
core-block-course-blocks {
--side-blocks-max-width: var(--custom-side-blocks-max-width, 30%);
--side-blocks-min-width: var(--custom-side-blocks-min-width, 280px);
}
--selected-item-color: var(--custom-selected-item-color, var(--core-color));
--selected-item-border-width: var(--custom-selected-item-border-width, 5px);