commit
b1a2a46217
|
@ -20,7 +20,7 @@
|
||||||
<core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="calendar" [message]="'addon.calendar.noevents' | translate">
|
<core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="calendar" [message]="'addon.calendar.noevents' | translate">
|
||||||
</core-empty-box>
|
</core-empty-box>
|
||||||
|
|
||||||
<ion-list *ngIf="filteredEvents && filteredEvents.length">
|
<ion-list *ngIf="filteredEvents && filteredEvents.length" no-margin>
|
||||||
<a ion-item text-wrap *ngFor="let event of filteredEvents" [title]="event.name" (click)="gotoEvent(event.id)" [class.core-split-item-selected]="event.id == eventId">
|
<a ion-item text-wrap *ngFor="let event of filteredEvents" [title]="event.name" (click)="gotoEvent(event.id)" [class.core-split-item-selected]="event.id == eventId">
|
||||||
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" item-start class="core-module-icon">
|
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" item-start class="core-module-icon">
|
||||||
<ion-icon *ngIf="!event.moduleIcon" name="{{event.icon}}" item-start></ion-icon>
|
<ion-icon *ngIf="!event.moduleIcon" name="{{event.icon}}" item-start></ion-icon>
|
||||||
|
|
|
@ -37,6 +37,8 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
|
||||||
import { CoreTabsComponent } from './tabs/tabs';
|
import { CoreTabsComponent } from './tabs/tabs';
|
||||||
import { CoreTabComponent } from './tabs/tab';
|
import { CoreTabComponent } from './tabs/tab';
|
||||||
import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor';
|
import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor';
|
||||||
|
import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
||||||
|
import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -59,7 +61,9 @@ import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor
|
||||||
CoreSitePickerComponent,
|
CoreSitePickerComponent,
|
||||||
CoreTabsComponent,
|
CoreTabsComponent,
|
||||||
CoreTabComponent,
|
CoreTabComponent,
|
||||||
CoreRichTextEditorComponent
|
CoreRichTextEditorComponent,
|
||||||
|
CoreNavBarButtonsComponent,
|
||||||
|
CoreDynamicComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
CoreContextMenuPopoverComponent,
|
CoreContextMenuPopoverComponent,
|
||||||
|
@ -89,7 +93,9 @@ import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor
|
||||||
CoreSitePickerComponent,
|
CoreSitePickerComponent,
|
||||||
CoreTabsComponent,
|
CoreTabsComponent,
|
||||||
CoreTabComponent,
|
CoreTabComponent,
|
||||||
CoreRichTextEditorComponent
|
CoreRichTextEditorComponent,
|
||||||
|
CoreNavBarButtonsComponent,
|
||||||
|
CoreDynamicComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
export class CoreComponentsModule {}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<!-- Content to display if no dynamic component. -->
|
||||||
|
<ng-content *ngIf="!instance"></ng-content>
|
||||||
|
|
||||||
|
<!-- Container of the dynamic component -->
|
||||||
|
<ng-container #dynamicComponent></ng-container>
|
|
@ -0,0 +1,168 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, ViewChild, OnInit, OnChanges, DoCheck, ViewContainerRef, ComponentFactoryResolver,
|
||||||
|
KeyValueDiffers, SimpleChange
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CoreLoggerProvider } from '../../providers/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to create another component dynamically.
|
||||||
|
*
|
||||||
|
* You need to pass the class of the component to this component (the class, not the name), along with the input data.
|
||||||
|
*
|
||||||
|
* So you should do something like:
|
||||||
|
*
|
||||||
|
* import { MyComponent } from './component';
|
||||||
|
*
|
||||||
|
* ...
|
||||||
|
*
|
||||||
|
* this.component = MyComponent;
|
||||||
|
*
|
||||||
|
* And in the template:
|
||||||
|
*
|
||||||
|
* <core-dynamic-component [component]="component" [data]="data">
|
||||||
|
* <p>Cannot render the data.</p>
|
||||||
|
* </core-dynamic-component>
|
||||||
|
*
|
||||||
|
* Please notice that the component that you pass needs to be declared in entryComponents of the module to be created dynamically.
|
||||||
|
*
|
||||||
|
* The contents of this component will be displayed if no component is supplied or it cannot be created. In the example above,
|
||||||
|
* if no component is supplied then the template will show the message "Cannot render the data.".
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-dynamic-component',
|
||||||
|
templateUrl: 'dynamic-component.html'
|
||||||
|
})
|
||||||
|
export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck {
|
||||||
|
|
||||||
|
@Input() component: any;
|
||||||
|
@Input() data: any;
|
||||||
|
|
||||||
|
// Get the container where to put the dynamic component.
|
||||||
|
@ViewChild('dynamicComponent', { read: ViewContainerRef }) set dynamicComponent(el: ViewContainerRef) {
|
||||||
|
this.container = el;
|
||||||
|
this.createComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
instance: any;
|
||||||
|
container: ViewContainerRef;
|
||||||
|
protected logger: any;
|
||||||
|
protected differ: any; // To detect changes in the data input.
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider, private factoryResolver: ComponentFactoryResolver, differs: KeyValueDiffers) {
|
||||||
|
this.logger = logger.getInstance('CoreDynamicComponent');
|
||||||
|
this.differ = differs.find([]).create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.createComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect changes on input properties.
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||||
|
if (!this.instance && changes.component) {
|
||||||
|
this.createComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays).
|
||||||
|
*/
|
||||||
|
ngDoCheck(): void {
|
||||||
|
if (this.instance) {
|
||||||
|
// Check if there's any change in the data object.
|
||||||
|
const changes = this.differ.diff(this.data);
|
||||||
|
if (changes) {
|
||||||
|
this.setInputData();
|
||||||
|
if (this.instance.ngOnChanges) {
|
||||||
|
this.instance.ngOnChanges(this.createChangesForComponent(changes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a component, add it to a container and set the input data.
|
||||||
|
*
|
||||||
|
* @return {boolean} Whether the component was successfully created.
|
||||||
|
*/
|
||||||
|
protected createComponent(): boolean {
|
||||||
|
if (!this.component || !this.container) {
|
||||||
|
// No component to instantiate or container doesn't exist right now.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.instance) {
|
||||||
|
// Component already instantiated.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create the component and add it to the container.
|
||||||
|
const factory = this.factoryResolver.resolveComponentFactory(this.component),
|
||||||
|
componentRef = this.container.createComponent(factory);
|
||||||
|
|
||||||
|
this.instance = componentRef.instance;
|
||||||
|
|
||||||
|
this.setInputData();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (ex) {
|
||||||
|
this.logger.error('Error creating component', ex);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the input data for the component.
|
||||||
|
*/
|
||||||
|
protected setInputData(): void {
|
||||||
|
for (const name in this.data) {
|
||||||
|
this.instance[name] = this.data[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the changes on the data input, create the changes object for the component.
|
||||||
|
*
|
||||||
|
* @param {any} changes Changes in the data input (detected by KeyValueDiffer).
|
||||||
|
* @return {{[name: string]: SimpleChange}} List of changes for the component.
|
||||||
|
*/
|
||||||
|
protected createChangesForComponent(changes: any): { [name: string]: SimpleChange } {
|
||||||
|
const newChanges: { [name: string]: SimpleChange } = {};
|
||||||
|
|
||||||
|
// Added items are considered first change.
|
||||||
|
changes.forEachAddedItem((item) => {
|
||||||
|
newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Changed or removed items aren't first change.
|
||||||
|
changes.forEachChangedItem((item) => {
|
||||||
|
newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, false);
|
||||||
|
});
|
||||||
|
changes.forEachRemovedItem((item) => {
|
||||||
|
newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return newChanges;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
core-navbar-buttons, .core-navbar-button-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, ContentChildren, ElementRef, QueryList } from '@angular/core';
|
||||||
|
import { Button } from 'ionic-angular';
|
||||||
|
import { CoreDomUtilsProvider } from '../../providers/utils/dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to add buttons to the app's header without having to place them inside the header itself. This is meant for
|
||||||
|
* pages that are loaded inside a sub ion-nav, so they don't have a header.
|
||||||
|
*
|
||||||
|
* If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that
|
||||||
|
* position. If no start/end is specified, then the buttons will be added to the first <ion-buttons> found in the header.
|
||||||
|
*
|
||||||
|
* You can use the [hidden] input to hide all the inner buttons if a certain condition is met.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* <core-navbar-buttons end>
|
||||||
|
* <button ion-button icon-only *ngIf="buttonShown" [attr.aria-label]="Do something" (click)="action()">
|
||||||
|
* <ion-icon name="funnel"></ion-icon>
|
||||||
|
* </button>
|
||||||
|
* </core-navbar-buttons>
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-navbar-buttons',
|
||||||
|
template: '<ng-content></ng-content>'
|
||||||
|
})
|
||||||
|
export class CoreNavBarButtonsComponent implements OnInit {
|
||||||
|
|
||||||
|
protected BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden';
|
||||||
|
|
||||||
|
// If the hidden input is true, hide all buttons.
|
||||||
|
@Input('hidden') set hidden(value: boolean) {
|
||||||
|
this._hidden = value;
|
||||||
|
if (this._buttons) {
|
||||||
|
this._buttons.forEach((button: Button) => {
|
||||||
|
this.showHideButton(button);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all the buttons inside this directive.
|
||||||
|
@ContentChildren(Button) set buttons(buttons: QueryList<Button>) {
|
||||||
|
this._buttons = buttons;
|
||||||
|
buttons.forEach((button: Button) => {
|
||||||
|
button.setRole('bar-button');
|
||||||
|
this.showHideButton(button);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected element: HTMLElement;
|
||||||
|
protected _buttons: QueryList<Button>;
|
||||||
|
protected _hidden: boolean;
|
||||||
|
|
||||||
|
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider) {
|
||||||
|
this.element = element.nativeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
const header = this.searchHeader();
|
||||||
|
|
||||||
|
if (header) {
|
||||||
|
// Search the right buttons container (start, end or any).
|
||||||
|
let selector = 'ion-buttons',
|
||||||
|
buttonsContainer: HTMLElement;
|
||||||
|
|
||||||
|
if (this.element.hasAttribute('start')) {
|
||||||
|
selector += '[start]';
|
||||||
|
} else if (this.element.hasAttribute('end')) {
|
||||||
|
selector += '[end]';
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonsContainer = <HTMLElement> header.querySelector(selector);
|
||||||
|
if (buttonsContainer) {
|
||||||
|
this.domUtils.moveChildren(this.element, buttonsContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search the ion-header where the buttons should be added.
|
||||||
|
*
|
||||||
|
* @return {HTMLElement} Header element.
|
||||||
|
*/
|
||||||
|
protected searchHeader(): HTMLElement {
|
||||||
|
let parentPage: HTMLElement = this.element;
|
||||||
|
|
||||||
|
while (parentPage) {
|
||||||
|
if (!parentPage.parentElement) {
|
||||||
|
// No parent, stop.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the next parent page.
|
||||||
|
parentPage = <HTMLElement> this.domUtils.closest(parentPage.parentElement, '.ion-page');
|
||||||
|
if (parentPage) {
|
||||||
|
// Check if the page has a header. If it doesn't, search the next parent page.
|
||||||
|
const header = this.searchHeaderInPage(parentPage);
|
||||||
|
if (header) {
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search ion-header inside a page. The header should be a direct child.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} page Page to search in.
|
||||||
|
* @return {HTMLElement} Header element. Undefined if not found.
|
||||||
|
*/
|
||||||
|
protected searchHeaderInPage(page: HTMLElement): HTMLElement {
|
||||||
|
for (let i = 0; i < page.children.length; i++) {
|
||||||
|
const child = page.children[i];
|
||||||
|
if (child.tagName == 'ION-HEADER') {
|
||||||
|
return <HTMLElement> child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show or hide a button.
|
||||||
|
*
|
||||||
|
* @param {Button} button Button to show or hide.
|
||||||
|
*/
|
||||||
|
protected showHideButton(button: Button): void {
|
||||||
|
if (this._hidden) {
|
||||||
|
button.getNativeElement().classList.add(this.BUTTON_HIDDEN_CLASS);
|
||||||
|
} else {
|
||||||
|
button.getNativeElement().classList.remove(this.BUTTON_HIDDEN_CLASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
<ion-split-pane (ionChange)="onSplitPaneChanged($event._visible);" [when]="when">
|
<ion-split-pane (ionChange)="onSplitPaneChanged($event._visible);" [when]="when">
|
||||||
<ion-menu [content]="detailNav" type="push">
|
<ion-menu [content]="detailNav" type="push">
|
||||||
<ion-header><ion-toolbar><ion-title></ion-title></ion-toolbar></ion-header>
|
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</ion-menu>
|
</ion-menu>
|
||||||
<ion-nav [root]="detailPage" #detailNav main></ion-nav>
|
<ion-nav [root]="detailPage" #detailNav main></ion-nav>
|
||||||
|
|
|
@ -36,4 +36,16 @@ core-split-view {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios ion-header + core-split-view ion-menu.split-pane-side ion-content{
|
||||||
|
top: $navbar-ios-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md ion-header + core-split-view ion-menu.split-pane-side ion-content{
|
||||||
|
top: $navbar-md-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp ion-header + core-split-view ion-menu.split-pane-side ion-content{
|
||||||
|
top: $navbar-wp-height;
|
||||||
}
|
}
|
|
@ -47,6 +47,7 @@ export class CoreSplitViewComponent implements OnInit {
|
||||||
@Input() when?: string | boolean = 'md';
|
@Input() when?: string | boolean = 'md';
|
||||||
protected isEnabled = false;
|
protected isEnabled = false;
|
||||||
protected masterPageName = '';
|
protected masterPageName = '';
|
||||||
|
protected masterPageIndex = 0;
|
||||||
protected loadDetailPage: any = false;
|
protected loadDetailPage: any = false;
|
||||||
protected element: HTMLElement; // Current element.
|
protected element: HTMLElement; // Current element.
|
||||||
|
|
||||||
|
@ -63,9 +64,32 @@ export class CoreSplitViewComponent implements OnInit {
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// Get the master page name and set an empty page as a placeholder.
|
// Get the master page name and set an empty page as a placeholder.
|
||||||
this.masterPageName = this.masterNav.getActive().component.name;
|
this.masterPageName = this.masterNav.getActive().component.name;
|
||||||
|
this.masterPageIndex = this.masterNav.indexOf(this.masterNav.getActive());
|
||||||
this.emptyDetails();
|
this.emptyDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the details NavController. If split view is not enabled, it will return the master nav.
|
||||||
|
*
|
||||||
|
* @return {NavController} Details NavController.
|
||||||
|
*/
|
||||||
|
getDetailsNav(): NavController {
|
||||||
|
if (this.isEnabled) {
|
||||||
|
return this.detailNav;
|
||||||
|
} else {
|
||||||
|
return this.masterNav;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the master NavController.
|
||||||
|
*
|
||||||
|
* @return {NavController} Master NavController.
|
||||||
|
*/
|
||||||
|
getMasterNav(): NavController {
|
||||||
|
return this.masterNav;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if both panels are shown. It depends on screen width.
|
* Check if both panels are shown. It depends on screen width.
|
||||||
*
|
*
|
||||||
|
@ -81,7 +105,7 @@ export class CoreSplitViewComponent implements OnInit {
|
||||||
* @param {any} page The component class or deeplink name you want to push onto the navigation stack.
|
* @param {any} page The component class or deeplink name you want to push onto the navigation stack.
|
||||||
* @param {any} params Any NavParams you want to pass along to the next view.
|
* @param {any} params Any NavParams you want to pass along to the next view.
|
||||||
*/
|
*/
|
||||||
push(page: any, params?: any, element?: HTMLElement): void {
|
push(page: any, params?: any): void {
|
||||||
if (this.isEnabled) {
|
if (this.isEnabled) {
|
||||||
this.detailNav.setRoot(page, params);
|
this.detailNav.setRoot(page, params);
|
||||||
} else {
|
} else {
|
||||||
|
@ -119,17 +143,19 @@ export class CoreSplitViewComponent implements OnInit {
|
||||||
activateSplitView(): void {
|
activateSplitView(): void {
|
||||||
const currentView = this.masterNav.getActive(),
|
const currentView = this.masterNav.getActive(),
|
||||||
currentPageName = currentView.component.name;
|
currentPageName = currentView.component.name;
|
||||||
if (currentPageName != this.masterPageName) {
|
if (this.masterNav.getPrevious().component.name == this.masterPageName) {
|
||||||
// CurrentView is a 'Detail' page remove it from the 'master' nav stack.
|
if (currentPageName != this.masterPageName) {
|
||||||
this.masterNav.pop();
|
// CurrentView is a 'Detail' page remove it from the 'master' nav stack.
|
||||||
|
this.masterNav.pop();
|
||||||
|
|
||||||
// And add it to the 'detail' nav stack.
|
// And add it to the 'detail' nav stack.
|
||||||
this.detailNav.setRoot(currentView.component, currentView.data);
|
this.detailNav.setRoot(currentView.component, currentView.data);
|
||||||
} else if (this.loadDetailPage) {
|
} else if (this.loadDetailPage) {
|
||||||
// MasterPage is shown, load the last detail page if found.
|
// MasterPage is shown, load the last detail page if found.
|
||||||
this.detailNav.setRoot(this.loadDetailPage.component, this.loadDetailPage.data);
|
this.detailNav.setRoot(this.loadDetailPage.component, this.loadDetailPage.data);
|
||||||
|
}
|
||||||
|
this.loadDetailPage = false;
|
||||||
}
|
}
|
||||||
this.loadDetailPage = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,7 +166,7 @@ export class CoreSplitViewComponent implements OnInit {
|
||||||
currentPageName = detailView.component.name;
|
currentPageName = detailView.component.name;
|
||||||
if (currentPageName != 'CoreSplitViewPlaceholderPage') {
|
if (currentPageName != 'CoreSplitViewPlaceholderPage') {
|
||||||
// Current detail view is a 'Detail' page so, not the placeholder page, push it on 'master' nav stack.
|
// Current detail view is a 'Detail' page so, not the placeholder page, push it on 'master' nav stack.
|
||||||
this.masterNav.push(detailView.component, detailView.data);
|
this.masterNav.insert(this.masterPageIndex + 1, detailView.component, detailView.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,10 +36,20 @@ core-tabs {
|
||||||
core-tab {
|
core-tab {
|
||||||
display: none;
|
display: none;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-content, .scroll-content {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ import { Content } from 'ionic-angular';
|
||||||
})
|
})
|
||||||
export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
|
export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
|
||||||
@Input() selectedIndex = 0; // Index of the tab to select.
|
@Input() selectedIndex = 0; // Index of the tab to select.
|
||||||
@Input() hideUntil: boolean; // Determine when should the contents be shown.
|
@Input() hideUntil = true; // Determine when should the contents be shown.
|
||||||
@Output() ionChange: EventEmitter<CoreTabComponent> = new EventEmitter<CoreTabComponent>(); // Emitted when the tab changes.
|
@Output() ionChange: EventEmitter<CoreTabComponent> = new EventEmitter<CoreTabComponent>(); // Emitted when the tab changes.
|
||||||
@ViewChild('originalTabs') originalTabsRef: ElementRef;
|
@ViewChild('originalTabs') originalTabsRef: ElementRef;
|
||||||
@ViewChild('topTabs') topTabs: ElementRef;
|
@ViewChild('topTabs') topTabs: ElementRef;
|
||||||
|
@ -60,13 +60,8 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
|
||||||
protected tabsShown = true;
|
protected tabsShown = true;
|
||||||
protected scroll: HTMLElement; // Parent scroll element (if core-tabs is inside a ion-content).
|
protected scroll: HTMLElement; // Parent scroll element (if core-tabs is inside a ion-content).
|
||||||
|
|
||||||
constructor(element: ElementRef, content: Content) {
|
constructor(element: ElementRef, protected content: Content) {
|
||||||
this.tabBarElement = element.nativeElement;
|
this.tabBarElement = element.nativeElement;
|
||||||
setTimeout(() => {
|
|
||||||
if (content) {
|
|
||||||
this.scroll = content.getScrollElement();
|
|
||||||
}
|
|
||||||
}, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -148,7 +143,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
|
||||||
let selectedIndex = this.selectedIndex || 0,
|
let selectedIndex = this.selectedIndex || 0,
|
||||||
selectedTab = this.tabs[selectedIndex];
|
selectedTab = this.tabs[selectedIndex];
|
||||||
|
|
||||||
if (!selectedTab.enabled || !selectedTab.show) {
|
if (!selectedTab || !selectedTab.enabled || !selectedTab.show) {
|
||||||
// The tab is not enabled or not shown. Get the first tab that is enabled.
|
// The tab is not enabled or not shown. Get the first tab that is enabled.
|
||||||
selectedTab = this.tabs.find((tab, index) => {
|
selectedTab = this.tabs.find((tab, index) => {
|
||||||
if (tab.enabled && tab.show) {
|
if (tab.enabled && tab.show) {
|
||||||
|
@ -168,8 +163,11 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
|
||||||
// Setup tab scrolling.
|
// Setup tab scrolling.
|
||||||
this.tabBarHeight = this.topTabsElement.offsetHeight;
|
this.tabBarHeight = this.topTabsElement.offsetHeight;
|
||||||
this.originalTabsContainer.style.paddingBottom = this.tabBarHeight + 'px';
|
this.originalTabsContainer.style.paddingBottom = this.tabBarHeight + 'px';
|
||||||
if (this.scroll) {
|
if (this.content) {
|
||||||
this.scroll.classList.add('no-scroll');
|
this.scroll = this.content.getScrollElement();
|
||||||
|
if (this.scroll) {
|
||||||
|
this.scroll.classList.add('no-scroll');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
|
|
|
@ -1,45 +1,45 @@
|
||||||
<!-- Default course format. -->
|
<!-- Default course format. -->
|
||||||
<div *ngIf="!componentInstances.courseFormat">
|
<core-dynamic-component [component]="courseFormatComponent" [data]="data">
|
||||||
<!-- Course summary. By default we only display the course progress. -->
|
<!-- Course summary. By default we only display the course progress. -->
|
||||||
<ion-list no-lines *ngIf="!componentInstances.courseSummary">
|
<core-dynamic-component [component]="courseSummaryComponent" [data]="data">
|
||||||
<ion-item *ngIf="course.progress != null && course.progress >= 0">
|
<ion-list no-lines>
|
||||||
<core-progress-bar [progress]="course.progress"></core-progress-bar>
|
<ion-item *ngIf="course.progress != null && course.progress >= 0">
|
||||||
</ion-item>
|
<core-progress-bar [progress]="course.progress"></core-progress-bar>
|
||||||
</ion-list>
|
</ion-item>
|
||||||
<ng-template #courseSummary></ng-template>
|
</ion-list>
|
||||||
|
</core-dynamic-component>
|
||||||
|
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<!-- Section selector. -->
|
<!-- Section selector. -->
|
||||||
<div *ngIf="!componentInstances.sectionSelector && displaySectionSelector && sections && sections.length" no-padding class="clearfix">
|
<core-dynamic-component [component]="sectionSelectorComponent" [data]="data">
|
||||||
<!-- @todo: How to display availabilityinfo and not visible messages? -->
|
<div *ngIf="displaySectionSelector && sections && sections.length" no-padding class="clearfix">
|
||||||
<ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions" float-start interface="popover">
|
<!-- @todo: How to display availabilityinfo and not visible messages? -->
|
||||||
<ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option>
|
<ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions" float-start interface="popover">
|
||||||
</ion-select>
|
<ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option>
|
||||||
<!-- Section download. -->
|
</ion-select>
|
||||||
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
|
<!-- Section download. -->
|
||||||
</div>
|
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
|
||||||
<ng-template #sectionSelector></ng-template>
|
</div>
|
||||||
|
</core-dynamic-component>
|
||||||
|
|
||||||
<!-- Single section. -->
|
<!-- Single section. -->
|
||||||
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
|
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
|
||||||
<ng-container *ngIf="!componentInstances.singleSection">
|
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
|
||||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
|
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
|
||||||
<core-empty-box *ngIf="!selectedSection.hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate"></core-empty-box>
|
<core-empty-box *ngIf="!selectedSection.hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate"></core-empty-box>
|
||||||
</ng-container>
|
</core-dynamic-component>
|
||||||
<ng-template #singleSection></ng-template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Multiple sections. -->
|
<!-- Multiple sections. -->
|
||||||
<div *ngIf="selectedSection && selectedSection.id == allSectionsId">
|
<div *ngIf="selectedSection && selectedSection.id == allSectionsId">
|
||||||
<ng-container *ngIf="!componentInstances.allSections">
|
<core-dynamic-component [component]="allSectionsComponent" [data]="data">
|
||||||
<ng-container *ngFor="let section of sections">
|
<ng-container *ngFor="let section of sections">
|
||||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container>
|
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</core-dynamic-component>
|
||||||
<ng-template #allSections></ng-template>
|
|
||||||
</div>
|
</div>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</div>
|
</core-dynamic-component>
|
||||||
|
|
||||||
<!-- Template to render a section. -->
|
<!-- Template to render a section. -->
|
||||||
<ng-template #sectionTemplate let-section="section">
|
<ng-template #sectionTemplate let-section="section">
|
||||||
|
@ -78,6 +78,3 @@
|
||||||
<ion-badge class="core-course-download-section-progress" *ngIf="section.isDownloading && section.total > 0 && section.count < section.total">{{section.count}} / {{section.total}}</ion-badge>
|
<ion-badge class="core-course-download-section-progress" *ngIf="section.isDownloading && section.total > 0 && section.count < section.total">{{section.count}} / {{section.total}}</ion-badge>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<!-- Custom course format that overrides the default one. -->
|
|
||||||
<ng-template #courseFormat></ng-template>
|
|
|
@ -12,13 +12,9 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import {
|
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter } from '@angular/core';
|
||||||
Component, Input, OnInit, OnChanges, OnDestroy, ViewContainerRef, ComponentFactoryResolver, ViewChild, ChangeDetectorRef,
|
|
||||||
SimpleChange, Output, EventEmitter
|
|
||||||
} from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { CoreEventsProvider } from '../../../../providers/events';
|
import { CoreEventsProvider } from '../../../../providers/events';
|
||||||
import { CoreLoggerProvider } from '../../../../providers/logger';
|
|
||||||
import { CoreSitesProvider } from '../../../../providers/sites';
|
import { CoreSitesProvider } from '../../../../providers/sites';
|
||||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||||
import { CoreCourseProvider } from '../../../course/providers/course';
|
import { CoreCourseProvider } from '../../../course/providers/course';
|
||||||
|
@ -48,31 +44,15 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
||||||
@Output() completionChanged?: EventEmitter<void>; // Will emit an event when any module completion changes.
|
@Output() completionChanged?: EventEmitter<void>; // Will emit an event when any module completion changes.
|
||||||
|
|
||||||
// Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf.
|
// All the possible component classes.
|
||||||
@ViewChild('courseFormat', { read: ViewContainerRef }) set courseFormat(el: ViewContainerRef) {
|
courseFormatComponent: any;
|
||||||
if (this.course) {
|
courseSummaryComponent: any;
|
||||||
this.createComponent('courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), el);
|
sectionSelectorComponent: any;
|
||||||
} else {
|
singleSectionComponent: any;
|
||||||
// The component hasn't been initialized yet. Store the container.
|
allSectionsComponent: any;
|
||||||
this.componentContainers['courseFormat'] = el;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ViewChild('courseSummary', { read: ViewContainerRef }) set courseSummary(el: ViewContainerRef) {
|
|
||||||
this.createComponent('courseSummary', this.cfDelegate.getCourseSummaryComponent(this.course), el);
|
|
||||||
}
|
|
||||||
@ViewChild('sectionSelector', { read: ViewContainerRef }) set sectionSelector(el: ViewContainerRef) {
|
|
||||||
this.createComponent('sectionSelector', this.cfDelegate.getSectionSelectorComponent(this.course), el);
|
|
||||||
}
|
|
||||||
@ViewChild('singleSection', { read: ViewContainerRef }) set singleSection(el: ViewContainerRef) {
|
|
||||||
this.createComponent('singleSection', this.cfDelegate.getSingleSectionComponent(this.course), el);
|
|
||||||
}
|
|
||||||
@ViewChild('allSections', { read: ViewContainerRef }) set allSections(el: ViewContainerRef) {
|
|
||||||
this.createComponent('allSections', this.cfDelegate.getAllSectionsComponent(this.course), el);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instances and containers of all the components that the handler could define.
|
// Data to pass to the components.
|
||||||
protected componentContainers: { [type: string]: ViewContainerRef } = {};
|
data: any = {};
|
||||||
componentInstances: { [type: string]: any } = {};
|
|
||||||
|
|
||||||
displaySectionSelector: boolean;
|
displaySectionSelector: boolean;
|
||||||
selectedSection: any;
|
selectedSection: any;
|
||||||
|
@ -80,23 +60,20 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
selectOptions: any = {};
|
selectOptions: any = {};
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
|
|
||||||
protected logger;
|
|
||||||
protected sectionStatusObserver;
|
protected sectionStatusObserver;
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider, private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService,
|
constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService,
|
||||||
private factoryResolver: ComponentFactoryResolver, private cdr: ChangeDetectorRef,
|
|
||||||
private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider,
|
private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider,
|
||||||
eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider,
|
eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider,
|
||||||
prefetchDelegate: CoreCourseModulePrefetchDelegate) {
|
prefetchDelegate: CoreCourseModulePrefetchDelegate) {
|
||||||
|
|
||||||
this.logger = logger.getInstance('CoreCourseFormatComponent');
|
|
||||||
this.selectOptions.title = translate.instant('core.course.sections');
|
this.selectOptions.title = translate.instant('core.course.sections');
|
||||||
this.completionChanged = new EventEmitter();
|
this.completionChanged = new EventEmitter();
|
||||||
|
|
||||||
// Listen for section status changes.
|
// Listen for section status changes.
|
||||||
this.sectionStatusObserver = eventsProvider.on(CoreEventsProvider.SECTION_STATUS_CHANGED, (data) => {
|
this.sectionStatusObserver = eventsProvider.on(CoreEventsProvider.SECTION_STATUS_CHANGED, (data) => {
|
||||||
if (this.downloadEnabled && this.sections && this.sections.length && this.course && data.sectionId &&
|
if (this.downloadEnabled && this.sections && this.sections.length && this.course && data.sectionId &&
|
||||||
data.courseId == this.course.id) {
|
data.courseId == this.course.id) {
|
||||||
// Check if the affected section is being downloaded.
|
// Check if the affected section is being downloaded.
|
||||||
// If so, we don't update section status because it'll already be updated when the download finishes.
|
// If so, we don't update section status because it'll already be updated when the download finishes.
|
||||||
const downloadId = this.courseHelper.getSectionDownloadId({ id: data.sectionId });
|
const downloadId = this.courseHelper.getSectionDownloadId({ id: data.sectionId });
|
||||||
|
@ -135,15 +112,19 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.displaySectionSelector = this.cfDelegate.displaySectionSelector(this.course);
|
this.displaySectionSelector = this.cfDelegate.displaySectionSelector(this.course);
|
||||||
|
|
||||||
this.createComponent(
|
|
||||||
'courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), this.componentContainers['courseFormat']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect changes on input properties.
|
* Detect changes on input properties.
|
||||||
*/
|
*/
|
||||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||||
|
this.setInputData();
|
||||||
|
|
||||||
|
if (changes.course) {
|
||||||
|
// Course has changed, try to get the components.
|
||||||
|
this.getComponents();
|
||||||
|
}
|
||||||
|
|
||||||
if (changes.sections && this.sections) {
|
if (changes.sections && this.sections) {
|
||||||
if (!this.selectedSection) {
|
if (!this.selectedSection) {
|
||||||
// There is no selected section yet, calculate which one to load.
|
// There is no selected section yet, calculate which one to load.
|
||||||
|
@ -186,62 +167,39 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
if (changes.downloadEnabled && this.downloadEnabled) {
|
if (changes.downloadEnabled && this.downloadEnabled) {
|
||||||
this.calculateSectionsStatus(false);
|
this.calculateSectionsStatus(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the changes to the components and call ngOnChanges if it exists.
|
|
||||||
for (const type in this.componentInstances) {
|
|
||||||
const instance = this.componentInstances[type];
|
|
||||||
|
|
||||||
for (const name in changes) {
|
|
||||||
instance[name] = changes[name].currentValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instance.ngOnChanges) {
|
|
||||||
instance.ngOnChanges(changes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a component, add it to a container and set the input data.
|
* Set the input data for components.
|
||||||
*
|
|
||||||
* @param {string} type The "type" of the component.
|
|
||||||
* @param {any} componentClass The class of the component to create.
|
|
||||||
* @param {ViewContainerRef} container The container to add the component to.
|
|
||||||
* @return {boolean} Whether the component was successfully created.
|
|
||||||
*/
|
*/
|
||||||
protected createComponent(type: string, componentClass: any, container: ViewContainerRef): boolean {
|
protected setInputData(): void {
|
||||||
if (!componentClass || !container) {
|
this.data.course = this.course;
|
||||||
// No component to instantiate or container doesn't exist right now.
|
this.data.sections = this.sections;
|
||||||
return false;
|
this.data.initialSectionId = this.initialSectionId;
|
||||||
}
|
this.data.initialSectionNumber = this.initialSectionNumber;
|
||||||
|
this.data.downloadEnabled = this.downloadEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.componentInstances[type] && container === this.componentContainers[type]) {
|
/**
|
||||||
// Component already instantiated and the component hasn't been destroyed, nothing to do.
|
* Get the components classes.
|
||||||
return true;
|
*/
|
||||||
}
|
protected getComponents(): void {
|
||||||
|
if (this.course) {
|
||||||
try {
|
if (!this.courseFormatComponent) {
|
||||||
// Create the component and add it to the container.
|
this.courseFormatComponent = this.cfDelegate.getCourseFormatComponent(this.course);
|
||||||
const factory = this.factoryResolver.resolveComponentFactory(componentClass),
|
}
|
||||||
componentRef = container.createComponent(factory);
|
if (!this.courseSummaryComponent) {
|
||||||
|
this.courseSummaryComponent = this.cfDelegate.getCourseSummaryComponent(this.course);
|
||||||
this.componentContainers[type] = container;
|
}
|
||||||
this.componentInstances[type] = componentRef.instance;
|
if (!this.sectionSelectorComponent) {
|
||||||
|
this.sectionSelectorComponent = this.cfDelegate.getSectionSelectorComponent(this.course);
|
||||||
// Set the Input data.
|
}
|
||||||
this.componentInstances[type].course = this.course;
|
if (!this.singleSectionComponent) {
|
||||||
this.componentInstances[type].sections = this.sections;
|
this.singleSectionComponent = this.cfDelegate.getSingleSectionComponent(this.course);
|
||||||
this.componentInstances[type].initialSectionId = this.initialSectionId;
|
}
|
||||||
this.componentInstances[type].initialSectionNumber = this.initialSectionNumber;
|
if (!this.allSectionsComponent) {
|
||||||
this.componentInstances[type].downloadEnabled = this.downloadEnabled;
|
this.allSectionsComponent = this.cfDelegate.getAllSectionsComponent(this.course);
|
||||||
|
}
|
||||||
this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed.
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (ex) {
|
|
||||||
this.logger.error('Error creating component', type, ex);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,16 +211,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
sectionChanged(newSection: any): void {
|
sectionChanged(newSection: any): void {
|
||||||
const previousValue = this.selectedSection;
|
const previousValue = this.selectedSection;
|
||||||
this.selectedSection = newSection;
|
this.selectedSection = newSection;
|
||||||
|
this.data.section = this.selectedSection;
|
||||||
// If there is a component to render the current section, update its section.
|
|
||||||
if (this.componentInstances.singleSection) {
|
|
||||||
this.componentInstances.singleSection.section = this.selectedSection;
|
|
||||||
if (this.componentInstances.singleSection.ngOnChanges) {
|
|
||||||
this.componentInstances.singleSection.ngOnChanges({
|
|
||||||
section: new SimpleChange(previousValue, newSection, typeof previousValue != 'undefined')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
// (C) Copyright 2015 Martin Dougiamas
|
|
||||||
//
|
|
||||||
// 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, OnChanges, ViewContainerRef, ComponentFactoryResolver, SimpleChange } from '@angular/core';
|
|
||||||
import { CoreLoggerProvider } from '../../../../../providers/logger';
|
|
||||||
import { CoreCourseModuleDelegate } from '../../../providers/module-delegate';
|
|
||||||
import { CoreCourseUnsupportedModuleComponent } from '../../../components/unsupported-module/unsupported-module';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component to display single activity format. It will determine the right component to use and instantiate it.
|
|
||||||
*
|
|
||||||
* The instantiated component will receive the course and the module as inputs.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'core-course-format-single-activity',
|
|
||||||
template: ''
|
|
||||||
})
|
|
||||||
export class CoreCourseFormatSingleActivityComponent implements OnChanges {
|
|
||||||
@Input() course: any; // The course to render.
|
|
||||||
@Input() sections: any[]; // List of course sections.
|
|
||||||
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
|
||||||
|
|
||||||
protected logger: any;
|
|
||||||
protected module: any;
|
|
||||||
protected componentInstance: any;
|
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider, private viewRef: ViewContainerRef, private factoryResolver: ComponentFactoryResolver,
|
|
||||||
private moduleDelegate: CoreCourseModuleDelegate) {
|
|
||||||
this.logger = logger.getInstance('CoreCourseFormatSingleActivityComponent');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect changes on input properties.
|
|
||||||
*/
|
|
||||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
|
||||||
if (this.course && this.sections && this.sections.length) {
|
|
||||||
// In single activity the module should only have 1 section and 1 module. Get the module.
|
|
||||||
const module = this.sections[0] && this.sections[0].modules && this.sections[0].modules[0];
|
|
||||||
if (module && !this.componentInstance) {
|
|
||||||
// We haven't created the component yet. Create it now.
|
|
||||||
this.createComponent(module);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.componentInstance && this.componentInstance.ngOnChanges) {
|
|
||||||
// Call ngOnChanges of the component.
|
|
||||||
const newChanges: { [name: string]: SimpleChange } = {};
|
|
||||||
|
|
||||||
// Check if course has changed.
|
|
||||||
if (changes.course) {
|
|
||||||
newChanges.course = changes.course;
|
|
||||||
this.componentInstance.course = this.course;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if module has changed.
|
|
||||||
if (changes.sections && module != this.module) {
|
|
||||||
newChanges.module = {
|
|
||||||
currentValue: module,
|
|
||||||
firstChange: changes.sections.firstChange,
|
|
||||||
previousValue: this.module,
|
|
||||||
isFirstChange: (): boolean => {
|
|
||||||
return newChanges.module.firstChange;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.componentInstance.module = module;
|
|
||||||
this.module = module;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(newChanges).length) {
|
|
||||||
this.componentInstance.ngOnChanges(newChanges);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the component, add it to the container and set the input data.
|
|
||||||
*
|
|
||||||
* @param {any} module The module.
|
|
||||||
* @return {boolean} Whether the component was successfully created.
|
|
||||||
*/
|
|
||||||
protected createComponent(module: any): boolean {
|
|
||||||
const componentClass = this.moduleDelegate.getMainComponent(this.course, module) || CoreCourseUnsupportedModuleComponent;
|
|
||||||
if (!componentClass) {
|
|
||||||
// No component to instantiate.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create the component and add it to the container.
|
|
||||||
const factory = this.factoryResolver.resolveComponentFactory(componentClass),
|
|
||||||
componentRef = this.viewRef.createComponent(factory);
|
|
||||||
|
|
||||||
this.componentInstance = componentRef.instance;
|
|
||||||
|
|
||||||
// Set the Input data.
|
|
||||||
this.componentInstance.courseId = this.course.id;
|
|
||||||
this.componentInstance.module = module;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (ex) {
|
|
||||||
this.logger.error('Error creating component', ex);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
|
|
@ -0,0 +1,55 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, OnChanges, SimpleChange } from '@angular/core';
|
||||||
|
import { CoreCourseModuleDelegate } from '../../../providers/module-delegate';
|
||||||
|
import { CoreCourseUnsupportedModuleComponent } from '../../../components/unsupported-module/unsupported-module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display single activity format. It will determine the right component to use and instantiate it.
|
||||||
|
*
|
||||||
|
* The instantiated component will receive the course and the module as inputs.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-course-format-single-activity',
|
||||||
|
templateUrl: 'singleactivity.html'
|
||||||
|
})
|
||||||
|
export class CoreCourseFormatSingleActivityComponent implements OnChanges {
|
||||||
|
@Input() course: any; // The course to render.
|
||||||
|
@Input() sections: any[]; // List of course sections.
|
||||||
|
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
||||||
|
|
||||||
|
componentClass: any; // The class of the component to render.
|
||||||
|
data: any = {}; // Data to pass to the component.
|
||||||
|
|
||||||
|
constructor(private moduleDelegate: CoreCourseModuleDelegate) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect changes on input properties.
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||||
|
if (this.course && this.sections && this.sections.length) {
|
||||||
|
// In single activity the module should only have 1 section and 1 module. Get the module.
|
||||||
|
const module = this.sections[0] && this.sections[0].modules && this.sections[0].modules[0];
|
||||||
|
if (module && !this.componentClass) {
|
||||||
|
// We haven't obtained the class yet. Get it now.
|
||||||
|
this.componentClass = this.moduleDelegate.getMainComponent(this.course, module) ||
|
||||||
|
CoreCourseUnsupportedModuleComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data.courseId = this.course.id;
|
||||||
|
this.data.module = module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CoreCourseFormatHandler } from '../../../providers/format-delegate';
|
import { CoreCourseFormatHandler } from '../../../providers/format-delegate';
|
||||||
import { CoreCourseFormatSingleActivityComponent } from '../components/format';
|
import { CoreCourseFormatSingleActivityComponent } from '../components/singleactivity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to support singleactivity course format.
|
* Handler to support singleactivity course format.
|
||||||
|
|
|
@ -13,15 +13,17 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CoreCourseFormatSingleActivityComponent } from './components/format';
|
import { CoreCourseFormatSingleActivityComponent } from './components/singleactivity';
|
||||||
import { CoreCourseFormatSingleActivityHandler } from './providers/handler';
|
import { CoreCourseFormatSingleActivityHandler } from './providers/handler';
|
||||||
import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
|
import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
|
||||||
|
import { CoreComponentsModule } from '../../../../components/components.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreCourseFormatSingleActivityComponent
|
CoreCourseFormatSingleActivityComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
CoreComponentsModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CoreCourseFormatSingleActivityHandler
|
CoreCourseFormatSingleActivityHandler
|
||||||
|
|
|
@ -11,16 +11,26 @@
|
||||||
</ion-navbar>
|
</ion-navbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-refresher [enabled]="dataLoaded" (ionRefresh)="doRefresh($event)">
|
<core-tabs>
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<!-- Course contents tab. -->
|
||||||
</ion-refresher>
|
<core-tab [title]="'core.course.contents' | translate">
|
||||||
|
<ng-template>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="dataLoaded" (ionRefresh)="doRefresh($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
<core-loading [hideUntil]="dataLoaded">
|
<core-loading [hideUntil]="dataLoaded">
|
||||||
<!-- @todo: Use core-tabs or a new component. core-tabs might initialize all tabs at start, so we might require a new component. -->
|
<core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber" [downloadEnabled]="downloadEnabled" (completionChanged)="onCompletionChange()"></core-course-format>
|
||||||
<div class="core-tabs-bar">
|
</core-loading>
|
||||||
<a aria-selected="true">{{ 'core.course.contents' | translate }}</a>
|
</ion-content>
|
||||||
<a *ngFor="let handler of courseHandlers">{{ handler.data.title || translate }}</a>
|
</ng-template>
|
||||||
</div>
|
</core-tab>
|
||||||
<core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber" [downloadEnabled]="downloadEnabled" (completionChanged)="onCompletionChange()"></core-course-format>
|
<!-- One tab per handler. -->
|
||||||
</core-loading>
|
<core-tab *ngFor="let handler of courseHandlers" [title]="handler.data.title | translate" class="{{handler.data.class}}">
|
||||||
|
<ng-template>
|
||||||
|
<core-dynamic-component [component]="handler.data.component" [data]="handlerData"></core-dynamic-component>
|
||||||
|
</ng-template>
|
||||||
|
</core-tab>
|
||||||
|
</core-tabs>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -43,6 +43,7 @@ export class CoreCourseSectionPage implements OnDestroy {
|
||||||
sectionId: number;
|
sectionId: number;
|
||||||
sectionNumber: number;
|
sectionNumber: number;
|
||||||
courseHandlers: CoreCourseOptionsHandlerToDisplay[];
|
courseHandlers: CoreCourseOptionsHandlerToDisplay[];
|
||||||
|
handlerData: any = {}; // Data to send to the handlers components.
|
||||||
dataLoaded: boolean;
|
dataLoaded: boolean;
|
||||||
downloadEnabled: boolean;
|
downloadEnabled: boolean;
|
||||||
downloadEnabledIcon = 'square-outline'; // Disabled by default.
|
downloadEnabledIcon = 'square-outline'; // Disabled by default.
|
||||||
|
@ -63,6 +64,7 @@ export class CoreCourseSectionPage implements OnDestroy {
|
||||||
this.course = navParams.get('course');
|
this.course = navParams.get('course');
|
||||||
this.sectionId = navParams.get('sectionId');
|
this.sectionId = navParams.get('sectionId');
|
||||||
this.sectionNumber = navParams.get('sectionNumber');
|
this.sectionNumber = navParams.get('sectionNumber');
|
||||||
|
this.handlerData.courseId = this.course.id;
|
||||||
|
|
||||||
// Get the title to display. We dont't have sections yet.
|
// Get the title to display. We dont't have sections yet.
|
||||||
this.title = courseFormatDelegate.getCourseTitle(this.course);
|
this.title = courseFormatDelegate.getCourseTitle(this.course);
|
||||||
|
@ -122,11 +124,15 @@ export class CoreCourseSectionPage implements OnDestroy {
|
||||||
*/
|
*/
|
||||||
protected loadData(refresh?: boolean): Promise<any> {
|
protected loadData(refresh?: boolean): Promise<any> {
|
||||||
// First of all, get the course because the data might have changed.
|
// First of all, get the course because the data might have changed.
|
||||||
return this.coursesProvider.getUserCourse(this.course.id).then((course) => {
|
return this.coursesProvider.getUserCourse(this.course.id).catch(() => {
|
||||||
|
// Error getting the course, probably guest access.
|
||||||
|
}).then((course) => {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
let promise;
|
let promise;
|
||||||
|
|
||||||
this.course = course;
|
if (course) {
|
||||||
|
this.course = course;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the completion status.
|
// Get the completion status.
|
||||||
if (this.course.enablecompletion === false) {
|
if (this.course.enablecompletion === false) {
|
||||||
|
|
|
@ -33,7 +33,6 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the handler is enabled for a certain course.
|
* Whether or not the handler is enabled for a certain course.
|
||||||
* For perfomance reasons, do NOT call WebServices in here, call them in shouldDisplayForCourse.
|
|
||||||
*
|
*
|
||||||
* @param {number} courseId The course ID.
|
* @param {number} courseId The course ID.
|
||||||
* @param {any} accessData Access type and data. Default, guest, ...
|
* @param {any} accessData Access type and data. Default, guest, ...
|
||||||
|
@ -43,17 +42,6 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler {
|
||||||
*/
|
*/
|
||||||
isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean>;
|
isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the handler should be displayed for a course. If not implemented, assume it's true.
|
|
||||||
*
|
|
||||||
* @param {number} courseId The course ID.
|
|
||||||
* @param {any} accessData Access type and data. Default, guest, ...
|
|
||||||
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
|
|
||||||
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
|
|
||||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
|
||||||
*/
|
|
||||||
shouldDisplayForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the data needed to render the handler.
|
* Returns the data needed to render the handler.
|
||||||
*
|
*
|
||||||
|
@ -91,12 +79,6 @@ export interface CoreCourseOptionsHandlerData {
|
||||||
*/
|
*/
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the icon to display for the handler.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
icon: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to add to the displayed handler.
|
* Class to add to the displayed handler.
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -104,11 +86,10 @@ export interface CoreCourseOptionsHandlerData {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action to perform when the handler is clicked.
|
* The component to render the handler. It must be the component class, not the name or an instance.
|
||||||
*
|
* When the component is created, it will receive the courseId as input.
|
||||||
* @param {any} course The course.
|
|
||||||
*/
|
*/
|
||||||
action(course: any): void;
|
component: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -282,38 +263,22 @@ export class CoreCourseOptionsDelegate extends CoreDelegate {
|
||||||
// Call getHandlersForAccess to make sure the handlers have been loaded.
|
// Call getHandlersForAccess to make sure the handlers have been loaded.
|
||||||
return this.getHandlersForAccess(course.id, refresh, accessData, course.navOptions, course.admOptions);
|
return this.getHandlersForAccess(course.id, refresh, accessData, course.navOptions, course.admOptions);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] = [],
|
const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] = [];
|
||||||
promises = [];
|
|
||||||
let promise;
|
|
||||||
|
|
||||||
this.coursesHandlers[course.id].enabledHandlers.forEach((handler) => {
|
this.coursesHandlers[course.id].enabledHandlers.forEach((handler) => {
|
||||||
if (handler.shouldDisplayForCourse) {
|
handlersToDisplay.push({
|
||||||
promise = Promise.resolve(handler.shouldDisplayForCourse(
|
data: handler.getDisplayData(course),
|
||||||
course.id, accessData, course.navOptions, course.admOptions));
|
priority: handler.priority,
|
||||||
} else {
|
prefetch: handler.prefetch
|
||||||
// Not implemented, assume it should be displayed.
|
|
||||||
promise = Promise.resolve(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
promises.push(promise.then((enabled) => {
|
|
||||||
if (enabled) {
|
|
||||||
handlersToDisplay.push({
|
|
||||||
data: handler.getDisplayData(course),
|
|
||||||
priority: handler.priority,
|
|
||||||
prefetch: handler.prefetch
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.utils.allPromises(promises).then(() => {
|
|
||||||
// Sort them by priority.
|
|
||||||
handlersToDisplay.sort((a, b) => {
|
|
||||||
return b.priority - a.priority;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return handlersToDisplay;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sort them by priority.
|
||||||
|
handlersToDisplay.sort((a, b) => {
|
||||||
|
return b.priority - a.priority;
|
||||||
|
});
|
||||||
|
|
||||||
|
return handlersToDisplay;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,8 +416,7 @@ export class CoreCourseOptionsDelegate extends CoreDelegate {
|
||||||
* @param {any} accessData Access type and data. Default, guest, ...
|
* @param {any} accessData Access type and data. Default, guest, ...
|
||||||
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
|
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
|
||||||
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
|
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
|
||||||
* @return {Promise} Resolved when updated.
|
* @return {Promise<any>} Resolved when updated.
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
updateHandlersForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): Promise<any> {
|
updateHandlersForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): Promise<any> {
|
||||||
const promises = [],
|
const promises = [],
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<core-loading [hideUntil]="dataLoaded">
|
<core-loading [hideUntil]="dataLoaded">
|
||||||
|
|
||||||
<ion-list *ngIf="course">
|
<ion-list *ngIf="course">
|
||||||
<a ion-item text-wrap (click)="openCourse()" [title]="course.fullname" [attr.detail-none]="!handlersShouldBeShown">
|
<a ion-item text-wrap (click)="openCourse()" [title]="course.fullname" [attr.detail-none]="!canAccessCourse">
|
||||||
<ion-icon name="ionic" item-start></ion-icon>
|
<ion-icon name="ionic" item-start></ion-icon>
|
||||||
<h2><core-format-text [text]="course.fullname"></core-format-text></h2>
|
<h2><core-format-text [text]="course.fullname"></core-format-text></h2>
|
||||||
<p *ngIf="course.categoryname">{{course.categoryname}}</p>
|
<p *ngIf="course.categoryname">{{course.categoryname}}</p>
|
||||||
|
@ -41,24 +41,15 @@
|
||||||
<ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled">
|
<ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled">
|
||||||
<p>{{ 'core.courses.notenrollable' | translate }}</p>
|
<p>{{ 'core.courses.notenrollable' | translate }}</p>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<a ion-item *ngIf="handlersShouldBeShown" (click)="prefetchCourse()" detail-none>
|
<a ion-item *ngIf="canAccessCourse" (click)="prefetchCourse()" detail-none>
|
||||||
<ion-icon *ngIf="prefetchCourseData.prefetchCourseIcon != 'spinner'" [name]="prefetchCourseData.prefetchCourseIcon" item-start></ion-icon>
|
<ion-icon *ngIf="prefetchCourseData.prefetchCourseIcon != 'spinner'" [name]="prefetchCourseData.prefetchCourseIcon" item-start></ion-icon>
|
||||||
<ion-spinner *ngIf="prefetchCourseData.prefetchCourseIcon == 'spinner'" item-start></ion-spinner>
|
<ion-spinner *ngIf="prefetchCourseData.prefetchCourseIcon == 'spinner'" item-start></ion-spinner>
|
||||||
<h2>{{ 'core.course.downloadcourse' | translate }}</h2>
|
<h2>{{ 'core.course.downloadcourse' | translate }}</h2>
|
||||||
</a>
|
</a>
|
||||||
<a ion-item (click)="openCourse()" [title]="course.fullname" *ngIf="handlersShouldBeShown">
|
<a ion-item (click)="openCourse()" [title]="course.fullname" *ngIf="canAccessCourse">
|
||||||
<ion-icon name="briefcase" item-start></ion-icon>
|
<ion-icon name="briefcase" item-start></ion-icon>
|
||||||
<h2>{{ 'core.course.contents' | translate }}</h2>
|
<h2>{{ 'core.course.contents' | translate }}</h2>
|
||||||
</a>
|
</a>
|
||||||
<div class="core-course-handlers" *ngIf="handlersShouldBeShown && course._handlers && course._handlers.length">
|
|
||||||
<a ion-item text-wrap *ngFor="let handler of course._handlers" class="core-courses-handler {{handler.class}}">
|
|
||||||
<ion-icon [name]="icon" item-start></ion-icon>
|
|
||||||
<h2><core-format-text [text]="title | translate"></core-format-text></h2>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ion-item class="core-loading-course-handlers" text-center *ngIf="handlersShouldBeShown && !handlersLoaded">
|
|
||||||
<ion-spinner></ion-spinner>
|
|
||||||
</ion-item>
|
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -36,8 +36,7 @@ import { CoreCourseHelperProvider } from '../../../course/providers/helper';
|
||||||
export class CoreCoursesCoursePreviewPage implements OnDestroy {
|
export class CoreCoursesCoursePreviewPage implements OnDestroy {
|
||||||
course: any;
|
course: any;
|
||||||
isEnrolled: boolean;
|
isEnrolled: boolean;
|
||||||
handlersShouldBeShown = true;
|
canAccessCourse = true;
|
||||||
handlersLoaded: boolean;
|
|
||||||
component = 'CoreCoursesCoursePreview';
|
component = 'CoreCoursesCoursePreview';
|
||||||
selfEnrolInstances: any[] = [];
|
selfEnrolInstances: any[] = [];
|
||||||
paypalEnabled: boolean;
|
paypalEnabled: boolean;
|
||||||
|
@ -206,19 +205,17 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
|
||||||
// Success retrieving the course, we can assume the user has permissions to view it.
|
// Success retrieving the course, we can assume the user has permissions to view it.
|
||||||
this.course.fullname = course.fullname || this.course.fullname;
|
this.course.fullname = course.fullname || this.course.fullname;
|
||||||
this.course.summary = course.summary || this.course.summary;
|
this.course.summary = course.summary || this.course.summary;
|
||||||
|
this.canAccessCourse = true;
|
||||||
return this.loadCourseHandlers(refresh, false);
|
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// The user is not an admin/manager. Check if we can provide guest access to the course.
|
// The user is not an admin/manager. Check if we can provide guest access to the course.
|
||||||
return this.canAccessAsGuest().then((passwordRequired) => {
|
return this.canAccessAsGuest().then((passwordRequired) => {
|
||||||
if (!passwordRequired) {
|
if (!passwordRequired) {
|
||||||
return this.loadCourseHandlers(refresh, true);
|
this.canAccessCourse = true;
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(null);
|
this.canAccessCourse = false;
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.course._handlers = [];
|
this.canAccessCourse = false;
|
||||||
this.handlersShouldBeShown = false;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
@ -226,25 +223,11 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load course nav handlers.
|
|
||||||
*
|
|
||||||
* @param {boolean} refresh Whether the user is refreshing the data.
|
|
||||||
* @param {boolean} guest Whether it's guest access.
|
|
||||||
*/
|
|
||||||
protected loadCourseHandlers(refresh: boolean, guest: boolean): Promise<any> {
|
|
||||||
return this.courseOptionsDelegate.getHandlersToDisplay(this.course, refresh, guest, true).then((handlers) => {
|
|
||||||
this.course._handlers = handlers;
|
|
||||||
this.handlersShouldBeShown = true;
|
|
||||||
this.handlersLoaded = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the course.
|
* Open the course.
|
||||||
*/
|
*/
|
||||||
openCourse(): void {
|
openCourse(): void {
|
||||||
if (!this.handlersShouldBeShown) {
|
if (!this.canAccessCourse) {
|
||||||
// Course cannot be opened.
|
// Course cannot be opened.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -435,8 +418,7 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
|
||||||
* Prefetch the course.
|
* Prefetch the course.
|
||||||
*/
|
*/
|
||||||
prefetchCourse(): void {
|
prefetchCourse(): void {
|
||||||
this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course, undefined, this.course._handlers)
|
this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course).catch((error) => {
|
||||||
.catch((error) => {
|
|
||||||
if (!this.pageDestroyed) {
|
if (!this.pageDestroyed) {
|
||||||
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,21 +16,33 @@ import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { IonicModule } from 'ionic-angular';
|
import { IonicModule } from 'ionic-angular';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreUserParticipantsComponent } from './participants/participants';
|
||||||
import { CoreUserProfileFieldComponent } from './user-profile-field/user-profile-field';
|
import { CoreUserProfileFieldComponent } from './user-profile-field/user-profile-field';
|
||||||
|
import { CoreComponentsModule } from '../../../components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '../../../directives/directives.module';
|
||||||
|
import { CorePipesModule } from '../../../pipes/pipes.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
CoreUserParticipantsComponent,
|
||||||
CoreUserProfileFieldComponent
|
CoreUserProfileFieldComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
TranslateModule.forChild(),
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CorePipesModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
CoreUserParticipantsComponent,
|
||||||
CoreUserProfileFieldComponent
|
CoreUserProfileFieldComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
CoreUserParticipantsComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreUserComponentsModule {}
|
export class CoreUserComponentsModule {}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<core-split-view>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="participantsLoaded" (ionRefresh)="refreshParticipants($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="participantsLoaded">
|
||||||
|
<core-empty-box *ngIf="participants && participants.length == 0" icon="person" [message]="'core.user.noparticipants' | translate">
|
||||||
|
</core-empty-box>
|
||||||
|
|
||||||
|
<ion-list *ngIf="participants && participants.length > 0" no-margin>
|
||||||
|
<a ion-item text-wrap *ngFor="let participant of participants" [title]="participant.fullname" (click)="gotoParticipant(participant.id)" [class.core-split-item-selected]="participant.id == participantId">
|
||||||
|
<ion-avatar item-start>
|
||||||
|
<img src="{{participant.profileimageurl}}" [alt]="'core.pictureof' | translate:{$a: participant.fullname}" core-external-content>
|
||||||
|
</ion-avatar>
|
||||||
|
<h2><core-format-text [text]="participant.fullname"></core-format-text></h2>
|
||||||
|
<p *ngIf="participant.lastaccess"><strong>{{ 'core.lastaccess' | translate }}: </strong>{{ participant.lastaccess * 1000 | coreFormatDate:"dfmediumdate"}}</p>
|
||||||
|
</a>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<ion-infinite-scroll [enabled]="canLoadMore" (ionInfinite)="$event.waitFor(fetchData())">
|
||||||
|
<ion-infinite-scroll-content></ion-infinite-scroll-content>
|
||||||
|
</ion-infinite-scroll>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
||||||
|
</core-split-view>
|
|
@ -0,0 +1,103 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, ViewChild, Input, OnInit } from '@angular/core';
|
||||||
|
import { Content, NavParams } from 'ionic-angular';
|
||||||
|
import { CoreUserProvider } from '../../providers/user';
|
||||||
|
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||||
|
import { CoreSplitViewComponent } from '../../../../components/split-view/split-view';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays the list of course participants.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-user-participants',
|
||||||
|
templateUrl: 'participants.html',
|
||||||
|
})
|
||||||
|
export class CoreUserParticipantsComponent implements OnInit {
|
||||||
|
@ViewChild(Content) content: Content;
|
||||||
|
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||||
|
|
||||||
|
@Input() courseId: number;
|
||||||
|
|
||||||
|
participantId: number;
|
||||||
|
participants = [];
|
||||||
|
canLoadMore = false;
|
||||||
|
participantsLoaded = false;
|
||||||
|
|
||||||
|
constructor(private userProvider: CoreUserProvider, private domUtils: CoreDomUtilsProvider) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View loaded.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Get first participants.
|
||||||
|
this.fetchData(true).then(() => {
|
||||||
|
if (!this.participantId && this.splitviewCtrl.isOn() && this.participants.length > 0) {
|
||||||
|
// Take first and load it.
|
||||||
|
this.gotoParticipant(this.participants[0].id);
|
||||||
|
}
|
||||||
|
// Add log in Moodle.
|
||||||
|
this.userProvider.logParticipantsView(this.courseId).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
this.participantsLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all the data required for the view.
|
||||||
|
*
|
||||||
|
* @param {boolean} [refresh] Empty events array first.
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
fetchData(refresh: boolean = false): Promise<any> {
|
||||||
|
const firstToGet = refresh ? 0 : this.participants.length;
|
||||||
|
|
||||||
|
return this.userProvider.getParticipants(this.courseId, firstToGet).then((data) => {
|
||||||
|
if (refresh) {
|
||||||
|
this.participants = data.participants;
|
||||||
|
} else {
|
||||||
|
this.participants = this.participants.concat(data.participants);
|
||||||
|
}
|
||||||
|
this.canLoadMore = data.canLoadMore;
|
||||||
|
}).catch((error) => {
|
||||||
|
this.domUtils.showErrorModalDefault(error, 'Error loading participants');
|
||||||
|
this.canLoadMore = false; // Set to false to prevent infinite calls with infinite-loading.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh data.
|
||||||
|
*
|
||||||
|
* @param {any} refresher Refresher.
|
||||||
|
*/
|
||||||
|
refreshParticipants(refresher: any): void {
|
||||||
|
this.userProvider.invalidateParticipantsList(this.courseId).finally(() => {
|
||||||
|
this.fetchData(true).finally(() => {
|
||||||
|
refresher.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a particular user profile.
|
||||||
|
* @param {number} userId User Id where to navigate.
|
||||||
|
*/
|
||||||
|
gotoParticipant(userId: number): void {
|
||||||
|
this.participantId = userId;
|
||||||
|
this.splitviewCtrl.push('CoreUserProfilePage', {userId: userId, courseId: this.courseId});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1 @@
|
||||||
<!-- User profile field that overrides the default one. -->
|
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
|
||||||
<ng-template #userProfileField></ng-template>
|
|
||||||
|
|
|
@ -12,8 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { CoreLoggerProvider } from '../../../../providers/logger';
|
|
||||||
import { CoreUserProfileFieldDelegate } from '../../providers/user-profile-field-delegate';
|
import { CoreUserProfileFieldDelegate } from '../../providers/user-profile-field-delegate';
|
||||||
import { CoreUtilsProvider } from '../../../../providers/utils/utils';
|
import { CoreUtilsProvider } from '../../../../providers/utils/utils';
|
||||||
|
|
||||||
|
@ -31,74 +30,23 @@ export class CoreUserProfileFieldComponent implements OnInit {
|
||||||
@Input() form?: any; // Form where to add the form control. Required if edit=true or signup=true.
|
@Input() form?: any; // Form where to add the form control. Required if edit=true or signup=true.
|
||||||
@Input() registerAuth?: string; // Register auth method. E.g. 'email'.
|
@Input() registerAuth?: string; // Register auth method. E.g. 'email'.
|
||||||
|
|
||||||
// Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf.
|
componentClass: any; // The class of the component to render.
|
||||||
@ViewChild('userProfileField', { read: ViewContainerRef }) set userProfileField(el: ViewContainerRef) {
|
data: any = {}; // Data to pass to the component.
|
||||||
if (this.field) {
|
|
||||||
this.createComponent(this.ufDelegate.getComponent(this.field, this.signup), el);
|
|
||||||
} else {
|
|
||||||
// The component hasn't been initialized yet. Store the container.
|
|
||||||
this.fieldContainer = el;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected logger;
|
constructor(private ufDelegate: CoreUserProfileFieldDelegate, private utilsProvider: CoreUtilsProvider) { }
|
||||||
|
|
||||||
// Instances and containers of all the components that the handler could define.
|
|
||||||
protected fieldContainer: ViewContainerRef;
|
|
||||||
protected fieldInstance: any;
|
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider, private factoryResolver: ComponentFactoryResolver,
|
|
||||||
private ufDelegate: CoreUserProfileFieldDelegate, private utilsProvider: CoreUtilsProvider) {
|
|
||||||
this.logger = logger.getInstance('CoreUserProfileFieldComponent');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.createComponent(this.ufDelegate.getComponent(this.field, this.signup), this.fieldContainer);
|
this.componentClass = this.ufDelegate.getComponent(this.field, this.signup);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.data.field = this.field;
|
||||||
* Create a component, add it to a container and set the input data.
|
this.data.edit = this.utilsProvider.isTrueOrOne(this.edit);
|
||||||
*
|
if (this.edit) {
|
||||||
* @param {any} componentClass The class of the component to create.
|
this.data.signup = this.utilsProvider.isTrueOrOne(this.signup);
|
||||||
* @param {ViewContainerRef} container The container to add the component to.
|
this.data.disabled = this.utilsProvider.isTrueOrOne(this.field.locked);
|
||||||
* @return {boolean} Whether the component was successfully created.
|
this.data.form = this.form;
|
||||||
*/
|
|
||||||
protected createComponent(componentClass: any, container: ViewContainerRef): boolean {
|
|
||||||
if (!componentClass || !container) {
|
|
||||||
// No component to instantiate or container doesn't exist right now.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.fieldInstance && container === this.fieldContainer) {
|
|
||||||
// Component already instantiated and the component hasn't been destroyed, nothing to do.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create the component and add it to the container.
|
|
||||||
const factory = this.factoryResolver.resolveComponentFactory(componentClass),
|
|
||||||
componentRef = container.createComponent(factory);
|
|
||||||
|
|
||||||
this.fieldContainer = container;
|
|
||||||
this.fieldInstance = componentRef.instance;
|
|
||||||
|
|
||||||
// Set the Input data.
|
|
||||||
this.fieldInstance.field = this.field;
|
|
||||||
this.fieldInstance.edit = this.utilsProvider.isTrueOrOne(this.edit);
|
|
||||||
if (this.edit) {
|
|
||||||
this.fieldInstance.signup = this.utilsProvider.isTrueOrOne(this.signup);
|
|
||||||
this.fieldInstance.disabled = this.utilsProvider.isTrueOrOne(this.field.locked);
|
|
||||||
this.fieldInstance.form = this.form;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (ex) {
|
|
||||||
this.logger.error('Error creating user field component', ex, componentClass);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
"lastname": "Surname",
|
"lastname": "Surname",
|
||||||
"manager": "Manager",
|
"manager": "Manager",
|
||||||
"newpicture": "New picture",
|
"newpicture": "New picture",
|
||||||
|
"noparticipants": "No participants found for this course.",
|
||||||
|
"participants": "Participants",
|
||||||
"phone1": "Phone",
|
"phone1": "Phone",
|
||||||
"phone2": "Mobile phone",
|
"phone2": "Mobile phone",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title>{{ 'core.user.participants' | translate }}</ion-title>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<core-user-participants [courseId]="courseId"></core-user-participants>
|
|
@ -0,0 +1,31 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { IonicPageModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreUserComponentsModule } from '../../components/components.module';
|
||||||
|
import { CoreUserParticipantsPage } from './participants';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreUserParticipantsPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreUserComponentsModule,
|
||||||
|
IonicPageModule.forChild(CoreUserParticipantsPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreUserParticipantsPageModule {}
|
|
@ -0,0 +1,32 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams } from 'ionic-angular';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the list of course participants.
|
||||||
|
*/
|
||||||
|
@IonicPage({segment: 'core-user-participants'})
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-user-participants',
|
||||||
|
templateUrl: 'participants.html',
|
||||||
|
})
|
||||||
|
export class CoreUserParticipantsPage {
|
||||||
|
courseId: number;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams) {
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,7 @@
|
||||||
<ion-grid ion-item class="core-user-communication-handlers" *ngIf="(communicationHandlers && communicationHandlers.length) || isLoadingHandlers">
|
<ion-grid ion-item class="core-user-communication-handlers" *ngIf="(communicationHandlers && communicationHandlers.length) || isLoadingHandlers">
|
||||||
<ion-row no-padding align-items-center text-center>
|
<ion-row no-padding align-items-center text-center>
|
||||||
<ion-col align-self-center *ngIf="communicationHandlers && communicationHandlers.length">
|
<ion-col align-self-center *ngIf="communicationHandlers && communicationHandlers.length">
|
||||||
<a *ngFor="let comHandler of communicationHandlers" (click)="comHandler.action($event, user, courseId)" [ngClass]="['core-user-profile-handler', comHandler.class]" title="{{comHandler.title | translate}}">
|
<a *ngFor="let comHandler of communicationHandlers" (click)="handlerClicked($event, comHandler)" [ngClass]="['core-user-profile-handler', comHandler.class]" title="{{comHandler.title | translate}}">
|
||||||
<ion-icon [name]="comHandler.icon"></ion-icon>
|
<ion-icon [name]="comHandler.icon"></ion-icon>
|
||||||
<p>{{comHandler.title | translate}}</p>
|
<p>{{comHandler.title | translate}}</p>
|
||||||
</a>
|
</a>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
</ion-row>
|
</ion-row>
|
||||||
</ion-grid>
|
</ion-grid>
|
||||||
|
|
||||||
<a ion-item text-wrap class="core-user-profile-handler" navPush="CoreUserAboutPage" [navParams]="{courseId: courseId, userId: userId}" title="{{ 'core.user.details' | translate }}">
|
<a ion-item text-wrap class="core-user-profile-handler" (click)="openUserDetails()" title="{{ 'core.user.details' | translate }}">
|
||||||
<ion-icon name="person" item-start></ion-icon>
|
<ion-icon name="person" item-start></ion-icon>
|
||||||
<h2>{{ 'core.user.details' | translate }}</h2>
|
<h2>{{ 'core.user.details' | translate }}</h2>
|
||||||
</a>
|
</a>
|
||||||
|
@ -45,13 +45,13 @@
|
||||||
<ion-spinner></ion-spinner>
|
<ion-spinner></ion-spinner>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<a *ngFor="let npHandler of newPageHandlers" ion-item text-wrap [ngClass]="['core-user-profile-handler', npHandler.class]" (click)="npHandler.action($event, user, courseId)" [hidden]="npHandler.hidden" title="{{ npHandler.title | translate }}">
|
<a *ngFor="let npHandler of newPageHandlers" ion-item text-wrap [ngClass]="['core-user-profile-handler', npHandler.class]" (click)="handlerClicked($event, npHandler)" [hidden]="npHandler.hidden" title="{{ npHandler.title | translate }}">
|
||||||
<ion-icon *ngIf="npHandler.icon" [name]="npHandler.icon" item-start></ion-icon>
|
<ion-icon *ngIf="npHandler.icon" [name]="npHandler.icon" item-start></ion-icon>
|
||||||
<h2>{{ npHandler.title | translate }}</h2>
|
<h2>{{ npHandler.title | translate }}</h2>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ion-item *ngIf="actionHandlers && actionHandlers.length">
|
<ion-item *ngIf="actionHandlers && actionHandlers.length">
|
||||||
<button *ngFor="let actHandler of actionHandlers" ion-button block outline [ngClass]="['core-user-profile-handler', actHandler.class]" (click)="actHandler.action($event, user, courseId)" [hidden]="actHandler.hidden" title="{{ actHandler.title | translate }}">
|
<button *ngFor="let actHandler of actionHandlers" ion-button block outline [ngClass]="['core-user-profile-handler', actHandler.class]" (click)="handlerClicked($event, actHandler)" [hidden]="actHandler.hidden" title="{{ actHandler.title | translate }}">
|
||||||
<ion-icon *ngIf="actHandler.icon" [name]="actHandler.icon" item-start></ion-icon>
|
<ion-icon *ngIf="actHandler.icon" [name]="actHandler.icon" item-start></ion-icon>
|
||||||
{{ actHandler.title | translate }}
|
{{ actHandler.title | translate }}
|
||||||
<ion-spinner *ngIf="actHandler.spinner" item-end></ion-spinner>
|
<ion-spinner *ngIf="actHandler.spinner" item-end></ion-spinner>
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, Optional } from '@angular/core';
|
||||||
import { IonicPage, NavParams } from 'ionic-angular';
|
import { IonicPage, NavParams, NavController } from 'ionic-angular';
|
||||||
import { CoreUserProvider } from '../../providers/user';
|
import { CoreUserProvider } from '../../providers/user';
|
||||||
import { CoreUserHelperProvider } from '../../providers/helper';
|
import { CoreUserHelperProvider } from '../../providers/helper';
|
||||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||||
|
@ -23,7 +23,8 @@ import { CoreEventsProvider } from '../../../../providers/events';
|
||||||
import { CoreSitesProvider } from '../../../../providers/sites';
|
import { CoreSitesProvider } from '../../../../providers/sites';
|
||||||
import { CoreMimetypeUtilsProvider } from '../../../../providers/utils/mimetype';
|
import { CoreMimetypeUtilsProvider } from '../../../../providers/utils/mimetype';
|
||||||
import { CoreFileUploaderHelperProvider } from '../../../fileuploader/providers/helper';
|
import { CoreFileUploaderHelperProvider } from '../../../fileuploader/providers/helper';
|
||||||
import { CoreUserDelegate } from '../../providers/user-delegate';
|
import { CoreUserDelegate, CoreUserProfileHandlerData } from '../../providers/user-delegate';
|
||||||
|
import { CoreSplitViewComponent } from '../../../../components/split-view/split-view';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays an user profile page.
|
* Page that displays an user profile page.
|
||||||
|
@ -45,15 +46,16 @@ export class CoreUserProfilePage {
|
||||||
title: string;
|
title: string;
|
||||||
isDeleted = false;
|
isDeleted = false;
|
||||||
canChangeProfilePicture = false;
|
canChangeProfilePicture = false;
|
||||||
actionHandlers = [];
|
actionHandlers: CoreUserProfileHandlerData[] = [];
|
||||||
newPageHandlers = [];
|
newPageHandlers: CoreUserProfileHandlerData[] = [];
|
||||||
communicationHandlers = [];
|
communicationHandlers: CoreUserProfileHandlerData[] = [];
|
||||||
|
|
||||||
constructor(navParams: NavParams, private userProvider: CoreUserProvider, private userHelper: CoreUserHelperProvider,
|
constructor(navParams: NavParams, private userProvider: CoreUserProvider, private userHelper: CoreUserHelperProvider,
|
||||||
private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private eventsProvider: CoreEventsProvider,
|
private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private eventsProvider: CoreEventsProvider,
|
||||||
private coursesProvider: CoreCoursesProvider, private sitesProvider: CoreSitesProvider,
|
private coursesProvider: CoreCoursesProvider, private sitesProvider: CoreSitesProvider,
|
||||||
private mimetypeUtils: CoreMimetypeUtilsProvider, private fileUploaderHelper: CoreFileUploaderHelperProvider,
|
private mimetypeUtils: CoreMimetypeUtilsProvider, private fileUploaderHelper: CoreFileUploaderHelperProvider,
|
||||||
private userDelegate: CoreUserDelegate) {
|
private userDelegate: CoreUserDelegate, private navCtrl: NavController,
|
||||||
|
@Optional() private svComponent: CoreSplitViewComponent) {
|
||||||
this.userId = navParams.get('userId');
|
this.userId = navParams.get('userId');
|
||||||
this.courseId = navParams.get('courseId');
|
this.courseId = navParams.get('courseId');
|
||||||
|
|
||||||
|
@ -181,6 +183,27 @@ export class CoreUserProfilePage {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the page with the user details.
|
||||||
|
*/
|
||||||
|
openUserDetails(): void {
|
||||||
|
// Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav.
|
||||||
|
const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
|
||||||
|
navCtrl.push('CoreUserAboutPage', {courseId: this.courseId, userId: this.userId});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A handler was clicked.
|
||||||
|
*
|
||||||
|
* @param {Event} event Click event.
|
||||||
|
* @param {CoreUserProfileHandlerData} handler Handler that was clicked.
|
||||||
|
*/
|
||||||
|
handlerClicked(event: Event, handler: CoreUserProfileHandlerData): void {
|
||||||
|
// Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav.
|
||||||
|
const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
|
||||||
|
handler.action(event, navCtrl, this.user, this.courseId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page destroyed.
|
* Page destroyed.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { NavController } from 'ionic-angular';
|
||||||
|
import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '../../course/providers/options-delegate';
|
||||||
|
import { CoreCourseProvider } from '../../course/providers/course';
|
||||||
|
import { CoreUserProvider } from './user';
|
||||||
|
import { CoreLoginHelperProvider } from '../../login/providers/helper';
|
||||||
|
import { CoreUserParticipantsComponent } from '../components/participants/participants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Course nav handler.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CoreUserParticipantsCourseOptionHandler implements CoreCourseOptionsHandler {
|
||||||
|
name = 'AddonParticipants';
|
||||||
|
priority = 600;
|
||||||
|
|
||||||
|
constructor(private userProvider: CoreUserProvider, private loginHelper: CoreLoginHelperProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should invalidate the data to determine if the handler is enabled for a certain course.
|
||||||
|
*
|
||||||
|
* @param {number} courseId The course ID.
|
||||||
|
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
|
||||||
|
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise<any> {
|
||||||
|
if (navOptions && typeof navOptions.participants != 'undefined') {
|
||||||
|
// No need to invalidate anything.
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userProvider.invalidateParticipantsList(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {boolean} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled for a certain course.
|
||||||
|
*
|
||||||
|
* @param {number} courseId The course ID.
|
||||||
|
* @param {any} accessData Access type and data. Default, guest, ...
|
||||||
|
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
|
||||||
|
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
|
||||||
|
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||||
|
*/
|
||||||
|
isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
|
||||||
|
if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) {
|
||||||
|
return false; // Not enabled for guests.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navOptions && typeof navOptions.participants != 'undefined') {
|
||||||
|
return navOptions.participants;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userProvider.isPluginEnabledForCourse(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data needed to render the handler.
|
||||||
|
*
|
||||||
|
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
|
||||||
|
*/
|
||||||
|
getDisplayData(): CoreCourseOptionsHandlerData {
|
||||||
|
return {
|
||||||
|
title: 'core.user.participants',
|
||||||
|
class: 'core-user-participants-handler',
|
||||||
|
component: CoreUserParticipantsComponent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { CoreContentLinksHandlerBase } from '../../../core/contentlinks/classes/base-handler';
|
||||||
|
import { CoreContentLinksAction } from '../../../core/contentlinks/providers/delegate';
|
||||||
|
import { CoreLoginHelperProvider } from '../../../core/login/providers/helper';
|
||||||
|
import { CoreUserProvider } from './user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to treat links to user participants page.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CoreUserParticipantsLinkHandler extends CoreContentLinksHandlerBase {
|
||||||
|
name = 'AddonParticipants';
|
||||||
|
featureName = '$mmCoursesDelegate_mmaParticipants';
|
||||||
|
pattern = /\/user\/index\.php/;
|
||||||
|
|
||||||
|
constructor(private userProvider: CoreUserProvider, private loginHelper: CoreLoginHelperProvider) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of actions for a link (url).
|
||||||
|
*
|
||||||
|
* @param {string[]} siteIds List of sites the URL belongs to.
|
||||||
|
* @param {string} url The URL to treat.
|
||||||
|
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||||
|
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
|
||||||
|
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
|
||||||
|
*/
|
||||||
|
getActions(siteIds: string[], url: string, params: any, courseId?: number):
|
||||||
|
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||||
|
courseId = parseInt(params.id, 10) || courseId;
|
||||||
|
|
||||||
|
return [{
|
||||||
|
action: (siteId, navCtrl?): void => {
|
||||||
|
// Always use redirect to make it the new history root (to avoid "loops" in history).
|
||||||
|
this.loginHelper.redirect('AddonParticipantsListPage', {courseId: courseId}, siteId);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled for a certain site (site + user) and a URL.
|
||||||
|
* If not defined, defaults to true.
|
||||||
|
*
|
||||||
|
* @param {string} siteId The site ID.
|
||||||
|
* @param {string} url The URL to treat.
|
||||||
|
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||||
|
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
|
||||||
|
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
|
||||||
|
*/
|
||||||
|
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
|
||||||
|
courseId = parseInt(params.id, 10) || courseId;
|
||||||
|
|
||||||
|
if (!courseId || url.indexOf('/grade/report/') != -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userProvider.isPluginEnabledForCourse(courseId, siteId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,12 +13,16 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { NavController } from 'ionic-angular';
|
||||||
import { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate';
|
import { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate';
|
||||||
import { CoreCoursesProvider } from '../../../core/courses/providers/courses';
|
import { CoreCoursesProvider } from '../../../core/courses/providers/courses';
|
||||||
import { CoreLoggerProvider } from '../../../providers/logger';
|
import { CoreLoggerProvider } from '../../../providers/logger';
|
||||||
import { CoreSitesProvider } from '../../../providers/sites';
|
import { CoreSitesProvider } from '../../../providers/sites';
|
||||||
import { CoreEventsProvider } from '../../../providers/events';
|
import { CoreEventsProvider } from '../../../providers/events';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that all user profile handlers must implement.
|
||||||
|
*/
|
||||||
export interface CoreUserProfileHandler extends CoreDelegateHandler {
|
export interface CoreUserProfileHandler extends CoreDelegateHandler {
|
||||||
/**
|
/**
|
||||||
* The highest priority is displayed first.
|
* The highest priority is displayed first.
|
||||||
|
@ -55,6 +59,9 @@ export interface CoreUserProfileHandler extends CoreDelegateHandler {
|
||||||
getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData;
|
getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data needed to render a user profile handler. It's returned by the handler.
|
||||||
|
*/
|
||||||
export interface CoreUserProfileHandlerData {
|
export interface CoreUserProfileHandlerData {
|
||||||
/**
|
/**
|
||||||
* Title to display.
|
* Title to display.
|
||||||
|
@ -88,12 +95,36 @@ export interface CoreUserProfileHandlerData {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action to do when clicked.
|
* Action to do when clicked.
|
||||||
* @param {any} $event
|
*
|
||||||
* @param {any} user User object.
|
* @param {Event} event Click event.
|
||||||
* @param {number} courseId Course ID where to show.
|
* @param {NavController} Nav controller to use to navigate.
|
||||||
* @return {any} Action to be done.
|
* @param {any} user User object.
|
||||||
|
* @param {number} [courseId] Course ID being viewed. If not defined, site context.
|
||||||
*/
|
*/
|
||||||
action?($event: any, user: any, courseId: number): any;
|
action?(event: Event, navCtrl: NavController, user: any, courseId?: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by the delegate for each handler.
|
||||||
|
*/
|
||||||
|
export interface CoreUserProfileHandlerToDisplay {
|
||||||
|
/**
|
||||||
|
* Data to display.
|
||||||
|
* @type {CoreUserProfileHandlerData}
|
||||||
|
*/
|
||||||
|
data: CoreUserProfileHandlerData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The highest priority is displayed first.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
priority?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the handler. See CoreUserProfileHandler.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -133,10 +164,10 @@ export class CoreUserDelegate extends CoreDelegate {
|
||||||
*
|
*
|
||||||
* @param {any} user The user object.
|
* @param {any} user The user object.
|
||||||
* @param {number} courseId The course ID.
|
* @param {number} courseId The course ID.
|
||||||
* @return {Promise<any>} Resolved with an array of objects containing 'priority', 'data' and 'type'.
|
* @return {Promise<CoreUserProfileHandlerToDisplay[]>} Resolved with the handlers.
|
||||||
*/
|
*/
|
||||||
getProfileHandlersFor(user: any, courseId: number): Promise<any> {
|
getProfileHandlersFor(user: any, courseId: number): Promise<CoreUserProfileHandlerToDisplay[]> {
|
||||||
const handlers = [],
|
const handlers: CoreUserProfileHandlerToDisplay[] = [],
|
||||||
promises = [];
|
promises = [];
|
||||||
|
|
||||||
// Retrieve course options forcing cache.
|
// Retrieve course options forcing cache.
|
||||||
|
|
|
@ -60,9 +60,9 @@ export class CoreUserProfileMailHandler implements CoreUserProfileHandler {
|
||||||
icon: 'mail',
|
icon: 'mail',
|
||||||
title: 'core.user.sendemail',
|
title: 'core.user.sendemail',
|
||||||
class: 'core-user-profile-mail',
|
class: 'core-user-profile-mail',
|
||||||
action: ($event, user, courseId): void => {
|
action: (event, navCtrl, user, courseId): void => {
|
||||||
$event.preventDefault();
|
event.preventDefault();
|
||||||
$event.stopPropagation();
|
event.stopPropagation();
|
||||||
window.open('mailto:' + user.email, '_blank');
|
window.open('mailto:' + user.email, '_blank');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { CoreUtilsProvider } from '../../../providers/utils/utils';
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CoreUserProvider {
|
export class CoreUserProvider {
|
||||||
|
static PARTICIPANTS_LIST_LIMIT = 50; // Max of participants to retrieve in each WS call.
|
||||||
static PROFILE_REFRESHED = 'CoreUserProfileRefreshed';
|
static PROFILE_REFRESHED = 'CoreUserProfileRefreshed';
|
||||||
static PROFILE_PICTURE_UPDATED = 'CoreUserProfilePictureUpdated';
|
static PROFILE_PICTURE_UPDATED = 'CoreUserProfilePictureUpdated';
|
||||||
protected ROOT_CACHE_KEY = 'mmUser:';
|
protected ROOT_CACHE_KEY = 'mmUser:';
|
||||||
|
@ -106,6 +107,59 @@ export class CoreUserProvider {
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get participants for a certain course.
|
||||||
|
*
|
||||||
|
* @param {number} courseId ID of the course.
|
||||||
|
* @param {number} limitFrom Position of the first participant to get.
|
||||||
|
* @param {number} limitNumber Number of participants to get.
|
||||||
|
* @param {string} [siteId] Site Id. If not defined, use current site.
|
||||||
|
* @return {Promise<any>} Promise to be resolved when the participants are retrieved.
|
||||||
|
*/
|
||||||
|
getParticipants(courseId: number, limitFrom: number = 0, limitNumber: number = CoreUserProvider.PARTICIPANTS_LIST_LIMIT,
|
||||||
|
siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
this.logger.debug(`Get participants for course '${courseId}' starting at '${limitFrom}'`);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
courseid: courseId,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'limitfrom',
|
||||||
|
value: limitFrom
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'limitnumber',
|
||||||
|
value: limitNumber
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sortby',
|
||||||
|
value: 'siteorder'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, preSets = {
|
||||||
|
cacheKey: this.getParticipantsListCacheKey(courseId)
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.read('core_enrol_get_enrolled_users', data, preSets).then((users) => {
|
||||||
|
const canLoadMore = users.length >= limitNumber;
|
||||||
|
this.storeUsers(users, siteId);
|
||||||
|
|
||||||
|
return { participants: users, canLoadMore: canLoadMore };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for participant list WS calls.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getParticipantsListCacheKey(courseId: number): string {
|
||||||
|
return this.ROOT_CACHE_KEY + 'list:' + courseId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user profile. The type of profile retrieved depends on the params.
|
* Get user profile. The type of profile retrieved depends on the params.
|
||||||
*
|
*
|
||||||
|
@ -218,6 +272,59 @@ export class CoreUserProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates participant list for a certain course.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {string} [siteId] Site Id. If not defined, use current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the list is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateParticipantsList(courseId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.invalidateWsCacheForKey(this.getParticipantsListCacheKey(courseId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if course participants is disabled in a certain site.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site Id. If not defined, use current site.
|
||||||
|
* @return {Promise<boolean>} Promise resolved with true if disabled, rejected or resolved with false otherwise.
|
||||||
|
*/
|
||||||
|
isParticipantsDisabled(siteId?: string): Promise<boolean> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return this.isParticipantsDisabledInSite(site);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if course participants is disabled in a certain site.
|
||||||
|
*
|
||||||
|
* @param {CoreSite} [site] Site. If not defined, use current site.
|
||||||
|
* @return {boolean} Whether it's disabled.
|
||||||
|
*/
|
||||||
|
isParticipantsDisabledInSite(site?: any): boolean {
|
||||||
|
site = site || this.sitesProvider.getCurrentSite();
|
||||||
|
|
||||||
|
return site.isFeatureDisabled('$mmCoursesDelegate_mmaParticipants');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not the participants addon is enabled for a certain course.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {string} [siteId] Site Id. If not defined, use current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
|
||||||
|
*/
|
||||||
|
isPluginEnabledForCourse(courseId: number, siteId?: string): Promise<any> {
|
||||||
|
if (!courseId) {
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieving one participant will fail if browsing users is disabled by capabilities.
|
||||||
|
return this.utils.promiseWorks(this.getParticipants(courseId, 0, 1, siteId));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if update profile picture is disabled in a certain site.
|
* Check if update profile picture is disabled in a certain site.
|
||||||
*
|
*
|
||||||
|
@ -248,6 +355,17 @@ export class CoreUserProvider {
|
||||||
return this.sitesProvider.getCurrentSite().write('core_user_view_user_profile', params);
|
return this.sitesProvider.getCurrentSite().write('core_user_view_user_profile', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log Participants list view in Moodle.
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
logParticipantsView(courseId?: number): Promise<any> {
|
||||||
|
return this.sitesProvider.getCurrentSite().write('core_user_view_user_list', {
|
||||||
|
courseid: courseId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store user basic information in local DB to be retrieved if the WS call fails.
|
* Store user basic information in local DB to be retrieved if the WS call fails.
|
||||||
*
|
*
|
||||||
|
@ -268,4 +386,23 @@ export class CoreUserProvider {
|
||||||
return site.getDb().insertOrUpdateRecord(this.USERS_TABLE, userRecord, { id: userId });
|
return site.getDb().insertOrUpdateRecord(this.USERS_TABLE, userRecord, { id: userId });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store users basic information in local DB.
|
||||||
|
*
|
||||||
|
* @param {any[]} users Users to store. Fields stored: id, fullname, profileimageurl.
|
||||||
|
* @param {string} [siteId] ID of the site. If not defined, use current site.
|
||||||
|
* @return {Promise<any>} Promise resolve when the user is stored.
|
||||||
|
*/
|
||||||
|
storeUsers(users: any[], siteId?: string): Promise<any> {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
users.forEach((user) => {
|
||||||
|
if (typeof user.id != 'undefined') {
|
||||||
|
promises.push(this.storeUser(user.id, user.fullname, user.profileimageurl, siteId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,11 +22,16 @@ import { CoreEventsProvider } from '../../providers/events';
|
||||||
import { CoreSitesProvider } from '../../providers/sites';
|
import { CoreSitesProvider } from '../../providers/sites';
|
||||||
import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate';
|
import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate';
|
||||||
import { CoreUserProfileLinkHandler } from './providers/user-link-handler';
|
import { CoreUserProfileLinkHandler } from './providers/user-link-handler';
|
||||||
|
import { CoreUserParticipantsCourseOptionHandler } from './providers/course-option-handler';
|
||||||
|
import { CoreUserParticipantsLinkHandler } from './providers/participants-link-handler';
|
||||||
|
import { CoreCourseOptionsDelegate } from '../course/providers/options-delegate';
|
||||||
|
import { CoreUserComponentsModule } from './components/components.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
CoreUserComponentsModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CoreUserDelegate,
|
CoreUserDelegate,
|
||||||
|
@ -34,15 +39,22 @@ import { CoreUserProfileLinkHandler } from './providers/user-link-handler';
|
||||||
CoreUserProfileMailHandler,
|
CoreUserProfileMailHandler,
|
||||||
CoreUserProvider,
|
CoreUserProvider,
|
||||||
CoreUserHelperProvider,
|
CoreUserHelperProvider,
|
||||||
CoreUserProfileLinkHandler
|
CoreUserProfileLinkHandler,
|
||||||
|
CoreUserParticipantsCourseOptionHandler,
|
||||||
|
CoreUserParticipantsLinkHandler
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreUserModule {
|
export class CoreUserModule {
|
||||||
constructor(userDelegate: CoreUserDelegate, userProfileMailHandler: CoreUserProfileMailHandler,
|
constructor(userDelegate: CoreUserDelegate, userProfileMailHandler: CoreUserProfileMailHandler,
|
||||||
eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, userProvider: CoreUserProvider,
|
eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, userProvider: CoreUserProvider,
|
||||||
contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler) {
|
contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler,
|
||||||
|
courseOptionHandler: CoreUserParticipantsCourseOptionHandler, linkHandler: CoreUserParticipantsLinkHandler,
|
||||||
|
courseOptionsDelegate: CoreCourseOptionsDelegate) {
|
||||||
|
|
||||||
userDelegate.registerHandler(userProfileMailHandler);
|
userDelegate.registerHandler(userProfileMailHandler);
|
||||||
|
courseOptionsDelegate.registerHandler(courseOptionHandler);
|
||||||
contentLinksDelegate.registerHandler(userLinkHandler);
|
contentLinksDelegate.registerHandler(userLinkHandler);
|
||||||
|
contentLinksDelegate.registerHandler(linkHandler);
|
||||||
|
|
||||||
eventsProvider.on(CoreEventsProvider.USER_DELETED, (data) => {
|
eventsProvider.on(CoreEventsProvider.USER_DELETED, (data) => {
|
||||||
// Search for userid in params.
|
// Search for userid in params.
|
||||||
|
|
|
@ -96,6 +96,7 @@
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"ios": "iOS",
|
"ios": "iOS",
|
||||||
"labelsep": ": ",
|
"labelsep": ": ",
|
||||||
|
"lastaccess": "Last access",
|
||||||
"lastdownloaded": "Last downloaded",
|
"lastdownloaded": "Last downloaded",
|
||||||
"lastmodified": "Last modified",
|
"lastmodified": "Last modified",
|
||||||
"lastsync": "Last synchronization",
|
"lastsync": "Last synchronization",
|
||||||
|
|
Loading…
Reference in New Issue