MOBILE-3608 blocks: Migrate basic block structure
parent
3356659a24
commit
7d1d318afc
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 can’t or won’t 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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
:host {
|
||||
ion-item-divider {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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.';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"blocks": "Blocks"
|
||||
}
|
|
@ -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) {}
|
||||
|
|
@ -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) {}
|
||||
|
|
@ -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';
|
||||
|
||||
}
|
|
@ -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, ...).
|
||||
*/
|
||||
|
|
|
@ -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).
|
||||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue