forked from EVOgeek/Vmeda.Online
		
	
						commit
						b1a2a46217
					
				| @ -20,7 +20,7 @@ | ||||
|             <core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="calendar" [message]="'addon.calendar.noevents' | translate"> | ||||
|             </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"> | ||||
|                     <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> | ||||
|  | ||||
| @ -37,6 +37,8 @@ import { CoreSitePickerComponent } from './site-picker/site-picker'; | ||||
| import { CoreTabsComponent } from './tabs/tabs'; | ||||
| import { CoreTabComponent } from './tabs/tab'; | ||||
| import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor'; | ||||
| import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons'; | ||||
| import { CoreDynamicComponent } from './dynamic-component/dynamic-component'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
| @ -59,7 +61,9 @@ import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor | ||||
|         CoreSitePickerComponent, | ||||
|         CoreTabsComponent, | ||||
|         CoreTabComponent, | ||||
|         CoreRichTextEditorComponent | ||||
|         CoreRichTextEditorComponent, | ||||
|         CoreNavBarButtonsComponent, | ||||
|         CoreDynamicComponent | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         CoreContextMenuPopoverComponent, | ||||
| @ -89,7 +93,9 @@ import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor | ||||
|         CoreSitePickerComponent, | ||||
|         CoreTabsComponent, | ||||
|         CoreTabComponent, | ||||
|         CoreRichTextEditorComponent | ||||
|         CoreRichTextEditorComponent, | ||||
|         CoreNavBarButtonsComponent, | ||||
|         CoreDynamicComponent | ||||
|     ] | ||||
| }) | ||||
| export class CoreComponentsModule {} | ||||
|  | ||||
							
								
								
									
										5
									
								
								src/components/dynamic-component/dynamic-component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/components/dynamic-component/dynamic-component.html
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||
							
								
								
									
										168
									
								
								src/components/dynamic-component/dynamic-component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								src/components/dynamic-component/dynamic-component.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/components/navbar-buttons/navbar-buttons.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/components/navbar-buttons/navbar-buttons.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| core-navbar-buttons, .core-navbar-button-hidden { | ||||
|     display: none !important; | ||||
| } | ||||
							
								
								
									
										148
									
								
								src/components/navbar-buttons/navbar-buttons.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								src/components/navbar-buttons/navbar-buttons.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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-menu [content]="detailNav" type="push"> | ||||
|         <ion-header><ion-toolbar><ion-title></ion-title></ion-toolbar></ion-header> | ||||
|         <ng-content></ng-content> | ||||
|     </ion-menu> | ||||
|     <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'; | ||||
|     protected isEnabled = false; | ||||
|     protected masterPageName = ''; | ||||
|     protected masterPageIndex = 0; | ||||
|     protected loadDetailPage: any = false; | ||||
|     protected element: HTMLElement; // Current element.
 | ||||
| 
 | ||||
| @ -63,9 +64,32 @@ export class CoreSplitViewComponent implements OnInit { | ||||
|     ngOnInit(): void { | ||||
|         // Get the master page name and set an empty page as a placeholder.
 | ||||
|         this.masterPageName = this.masterNav.getActive().component.name; | ||||
|         this.masterPageIndex = this.masterNav.indexOf(this.masterNav.getActive()); | ||||
|         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. | ||||
|      * | ||||
| @ -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} 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) { | ||||
|             this.detailNav.setRoot(page, params); | ||||
|         } else { | ||||
| @ -119,17 +143,19 @@ export class CoreSplitViewComponent implements OnInit { | ||||
|     activateSplitView(): void { | ||||
|         const currentView = this.masterNav.getActive(), | ||||
|             currentPageName = currentView.component.name; | ||||
|         if (currentPageName != this.masterPageName) { | ||||
|             // CurrentView is a 'Detail' page remove it from the 'master' nav stack.
 | ||||
|             this.masterNav.pop(); | ||||
|         if (this.masterNav.getPrevious().component.name == this.masterPageName) { | ||||
|             if (currentPageName != this.masterPageName) { | ||||
|                 // CurrentView is a 'Detail' page remove it from the 'master' nav stack.
 | ||||
|                 this.masterNav.pop(); | ||||
| 
 | ||||
|             // And add it to the 'detail' nav stack.
 | ||||
|             this.detailNav.setRoot(currentView.component, currentView.data); | ||||
|         } else if (this.loadDetailPage) { | ||||
|             // MasterPage is shown, load the last detail page if found.
 | ||||
|             this.detailNav.setRoot(this.loadDetailPage.component, this.loadDetailPage.data); | ||||
|                 // And add it to the 'detail' nav stack.
 | ||||
|                 this.detailNav.setRoot(currentView.component, currentView.data); | ||||
|             } else if (this.loadDetailPage) { | ||||
|                 // MasterPage is shown, load the last detail page if found.
 | ||||
|                 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; | ||||
|         if (currentPageName != 'CoreSplitViewPlaceholderPage') { | ||||
|             // 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 { | ||||
|         display: none; | ||||
|         height: 100%; | ||||
|         position: relative; | ||||
| 
 | ||||
|         &.selected { | ||||
|             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 { | ||||
|     @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.
 | ||||
|     @ViewChild('originalTabs') originalTabsRef: ElementRef; | ||||
|     @ViewChild('topTabs') topTabs: ElementRef; | ||||
| @ -60,13 +60,8 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { | ||||
|     protected tabsShown = true; | ||||
|     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; | ||||
|         setTimeout(() => { | ||||
|             if (content) { | ||||
|                 this.scroll = content.getScrollElement(); | ||||
|             } | ||||
|         }, 1); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -148,7 +143,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { | ||||
|         let selectedIndex = this.selectedIndex || 0, | ||||
|             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.
 | ||||
|             selectedTab = this.tabs.find((tab, index) => { | ||||
|                 if (tab.enabled && tab.show) { | ||||
| @ -168,8 +163,11 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { | ||||
|         // Setup tab scrolling.
 | ||||
|         this.tabBarHeight = this.topTabsElement.offsetHeight; | ||||
|         this.originalTabsContainer.style.paddingBottom = this.tabBarHeight + 'px'; | ||||
|         if (this.scroll) { | ||||
|             this.scroll.classList.add('no-scroll'); | ||||
|         if (this.content) { | ||||
|             this.scroll = this.content.getScrollElement(); | ||||
|             if (this.scroll) { | ||||
|                 this.scroll.classList.add('no-scroll'); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.initialized = true; | ||||
|  | ||||
| @ -1,45 +1,45 @@ | ||||
| <!-- Default course format. --> | ||||
| <div *ngIf="!componentInstances.courseFormat"> | ||||
| <core-dynamic-component [component]="courseFormatComponent" [data]="data"> | ||||
|     <!-- Course summary. By default we only display the course progress. --> | ||||
|     <ion-list no-lines *ngIf="!componentInstances.courseSummary"> | ||||
|         <ion-item *ngIf="course.progress != null && course.progress >= 0"> | ||||
|             <core-progress-bar [progress]="course.progress"></core-progress-bar> | ||||
|         </ion-item> | ||||
|     </ion-list> | ||||
|     <ng-template #courseSummary></ng-template> | ||||
|     <core-dynamic-component [component]="courseSummaryComponent" [data]="data"> | ||||
|         <ion-list no-lines> | ||||
|             <ion-item *ngIf="course.progress != null && course.progress >= 0"> | ||||
|                 <core-progress-bar [progress]="course.progress"></core-progress-bar> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
|     </core-dynamic-component> | ||||
| 
 | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <!-- Section selector. --> | ||||
|         <div *ngIf="!componentInstances.sectionSelector && displaySectionSelector && sections && sections.length" no-padding class="clearfix"> | ||||
|             <!-- @todo: How to display availabilityinfo and not visible messages? --> | ||||
|             <ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions" float-start interface="popover"> | ||||
|                 <ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option> | ||||
|             </ion-select> | ||||
|             <!-- Section download. --> | ||||
|             <ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container> | ||||
|         </div> | ||||
|         <ng-template #sectionSelector></ng-template> | ||||
|         <core-dynamic-component [component]="sectionSelectorComponent" [data]="data"> | ||||
|             <div *ngIf="displaySectionSelector && sections && sections.length" no-padding class="clearfix"> | ||||
|                 <!-- @todo: How to display availabilityinfo and not visible messages? --> | ||||
|                 <ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions" float-start interface="popover"> | ||||
|                     <ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option> | ||||
|                 </ion-select> | ||||
|                 <!-- Section download. --> | ||||
|                 <ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container> | ||||
|             </div> | ||||
|         </core-dynamic-component> | ||||
| 
 | ||||
|         <!-- Single section. --> | ||||
|         <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> | ||||
|                 <core-empty-box *ngIf="!selectedSection.hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate"></core-empty-box> | ||||
|             </ng-container> | ||||
|             <ng-template #singleSection></ng-template> | ||||
|             </core-dynamic-component> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Multiple sections. --> | ||||
|         <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 *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container> | ||||
|                 </ng-container> | ||||
|             </ng-container> | ||||
|             <ng-template #allSections></ng-template> | ||||
|             </core-dynamic-component> | ||||
|         </div> | ||||
|     </core-loading> | ||||
| </div> | ||||
| </core-dynamic-component> | ||||
| 
 | ||||
| <!-- Template to render a 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> | ||||
|     </div> | ||||
| </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
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { | ||||
|     Component, Input, OnInit, OnChanges, OnDestroy, ViewContainerRef, ComponentFactoryResolver, ViewChild, ChangeDetectorRef, | ||||
|     SimpleChange, Output, EventEmitter | ||||
| } from '@angular/core'; | ||||
| import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter } from '@angular/core'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreEventsProvider } from '../../../../providers/events'; | ||||
| import { CoreLoggerProvider } from '../../../../providers/logger'; | ||||
| import { CoreSitesProvider } from '../../../../providers/sites'; | ||||
| import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; | ||||
| 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).
 | ||||
|     @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.
 | ||||
|     @ViewChild('courseFormat', { read: ViewContainerRef }) set courseFormat(el: ViewContainerRef) { | ||||
|         if (this.course) { | ||||
|             this.createComponent('courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), el); | ||||
|         } else { | ||||
|             // The component hasn't been initialized yet. Store the container.
 | ||||
|             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); | ||||
|     } | ||||
|     // All the possible component classes.
 | ||||
|     courseFormatComponent: any; | ||||
|     courseSummaryComponent: any; | ||||
|     sectionSelectorComponent: any; | ||||
|     singleSectionComponent: any; | ||||
|     allSectionsComponent: any; | ||||
| 
 | ||||
|     // Instances and containers of all the components that the handler could define.
 | ||||
|     protected componentContainers: { [type: string]: ViewContainerRef } = {}; | ||||
|     componentInstances: { [type: string]: any } = {}; | ||||
|     // Data to pass to the components.
 | ||||
|     data: any = {}; | ||||
| 
 | ||||
|     displaySectionSelector: boolean; | ||||
|     selectedSection: any; | ||||
| @ -80,23 +60,20 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     selectOptions: any = {}; | ||||
|     loaded: boolean; | ||||
| 
 | ||||
|     protected logger; | ||||
|     protected sectionStatusObserver; | ||||
| 
 | ||||
|     constructor(logger: CoreLoggerProvider, private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, | ||||
|             private factoryResolver: ComponentFactoryResolver, private cdr: ChangeDetectorRef, | ||||
|     constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, | ||||
|             private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider, | ||||
|             eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider, | ||||
|             prefetchDelegate: CoreCourseModulePrefetchDelegate) { | ||||
| 
 | ||||
|         this.logger = logger.getInstance('CoreCourseFormatComponent'); | ||||
|         this.selectOptions.title = translate.instant('core.course.sections'); | ||||
|         this.completionChanged = new EventEmitter(); | ||||
| 
 | ||||
|         // Listen for section status changes.
 | ||||
|         this.sectionStatusObserver = eventsProvider.on(CoreEventsProvider.SECTION_STATUS_CHANGED, (data) => { | ||||
|             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.
 | ||||
|                 // 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 }); | ||||
| @ -135,15 +112,19 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.displaySectionSelector = this.cfDelegate.displaySectionSelector(this.course); | ||||
| 
 | ||||
|         this.createComponent( | ||||
|             'courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), this.componentContainers['courseFormat']); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Detect changes on input properties. | ||||
|      */ | ||||
|     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 (!this.selectedSection) { | ||||
|                 // 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) { | ||||
|             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. | ||||
|      * | ||||
|      * @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. | ||||
|      * Set the input data for components. | ||||
|      */ | ||||
|     protected createComponent(type: string, componentClass: any, container: ViewContainerRef): boolean { | ||||
|         if (!componentClass || !container) { | ||||
|             // No component to instantiate or container doesn't exist right now.
 | ||||
|             return false; | ||||
|         } | ||||
|     protected setInputData(): void { | ||||
|         this.data.course = this.course; | ||||
|         this.data.sections = this.sections; | ||||
|         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.
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             // Create the component and add it to the container.
 | ||||
|             const factory = this.factoryResolver.resolveComponentFactory(componentClass), | ||||
|                 componentRef = container.createComponent(factory); | ||||
| 
 | ||||
|             this.componentContainers[type] = container; | ||||
|             this.componentInstances[type] = componentRef.instance; | ||||
| 
 | ||||
|             // Set the Input data.
 | ||||
|             this.componentInstances[type].course = this.course; | ||||
|             this.componentInstances[type].sections = this.sections; | ||||
|             this.componentInstances[type].initialSectionId = this.initialSectionId; | ||||
|             this.componentInstances[type].initialSectionNumber = this.initialSectionNumber; | ||||
|             this.componentInstances[type].downloadEnabled = this.downloadEnabled; | ||||
| 
 | ||||
|             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; | ||||
|     /** | ||||
|      * Get the components classes. | ||||
|      */ | ||||
|     protected getComponents(): void { | ||||
|         if (this.course) { | ||||
|             if (!this.courseFormatComponent) { | ||||
|                 this.courseFormatComponent = this.cfDelegate.getCourseFormatComponent(this.course); | ||||
|             } | ||||
|             if (!this.courseSummaryComponent) { | ||||
|                 this.courseSummaryComponent = this.cfDelegate.getCourseSummaryComponent(this.course); | ||||
|             } | ||||
|             if (!this.sectionSelectorComponent) { | ||||
|                 this.sectionSelectorComponent = this.cfDelegate.getSectionSelectorComponent(this.course); | ||||
|             } | ||||
|             if (!this.singleSectionComponent) { | ||||
|                 this.singleSectionComponent = this.cfDelegate.getSingleSectionComponent(this.course); | ||||
|             } | ||||
|             if (!this.allSectionsComponent) { | ||||
|                 this.allSectionsComponent = this.cfDelegate.getAllSectionsComponent(this.course); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -253,16 +211,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     sectionChanged(newSection: any): void { | ||||
|         const previousValue = this.selectedSection; | ||||
|         this.selectedSection = newSection; | ||||
| 
 | ||||
|         // 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') | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|         this.data.section = this.selectedSection; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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 { CoreCourseFormatHandler } from '../../../providers/format-delegate'; | ||||
| import { CoreCourseFormatSingleActivityComponent } from '../components/format'; | ||||
| import { CoreCourseFormatSingleActivityComponent } from '../components/singleactivity'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to support singleactivity course format. | ||||
|  | ||||
| @ -13,15 +13,17 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CoreCourseFormatSingleActivityComponent } from './components/format'; | ||||
| import { CoreCourseFormatSingleActivityComponent } from './components/singleactivity'; | ||||
| import { CoreCourseFormatSingleActivityHandler } from './providers/handler'; | ||||
| import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; | ||||
| import { CoreComponentsModule } from '../../../../components/components.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         CoreCourseFormatSingleActivityComponent | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreComponentsModule | ||||
|     ], | ||||
|     providers: [ | ||||
|         CoreCourseFormatSingleActivityHandler | ||||
|  | ||||
| @ -11,16 +11,26 @@ | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <ion-refresher [enabled]="dataLoaded" (ionRefresh)="doRefresh($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-tabs> | ||||
|         <!-- Course contents tab. --> | ||||
|         <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"> | ||||
|         <!-- @todo: Use core-tabs or a new component. core-tabs might initialize all tabs at start, so we might require a new component. --> | ||||
|         <div class="core-tabs-bar"> | ||||
|             <a aria-selected="true">{{ 'core.course.contents' | translate }}</a> | ||||
|             <a *ngFor="let handler of courseHandlers">{{ handler.data.title || translate }}</a> | ||||
|         </div> | ||||
|         <core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber" [downloadEnabled]="downloadEnabled" (completionChanged)="onCompletionChange()"></core-course-format> | ||||
|     </core-loading> | ||||
|                     <core-loading [hideUntil]="dataLoaded"> | ||||
|                         <core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber" [downloadEnabled]="downloadEnabled" (completionChanged)="onCompletionChange()"></core-course-format> | ||||
|                     </core-loading> | ||||
|                 </ion-content> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
|         <!-- One tab per handler. --> | ||||
|         <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> | ||||
|  | ||||
| @ -43,6 +43,7 @@ export class CoreCourseSectionPage implements OnDestroy { | ||||
|     sectionId: number; | ||||
|     sectionNumber: number; | ||||
|     courseHandlers: CoreCourseOptionsHandlerToDisplay[]; | ||||
|     handlerData: any = {}; // Data to send to the handlers components.
 | ||||
|     dataLoaded: boolean; | ||||
|     downloadEnabled: boolean; | ||||
|     downloadEnabledIcon = 'square-outline'; // Disabled by default.
 | ||||
| @ -63,6 +64,7 @@ export class CoreCourseSectionPage implements OnDestroy { | ||||
|         this.course = navParams.get('course'); | ||||
|         this.sectionId = navParams.get('sectionId'); | ||||
|         this.sectionNumber = navParams.get('sectionNumber'); | ||||
|         this.handlerData.courseId = this.course.id; | ||||
| 
 | ||||
|         // Get the title to display. We dont't have sections yet.
 | ||||
|         this.title = courseFormatDelegate.getCourseTitle(this.course); | ||||
| @ -122,11 +124,15 @@ export class CoreCourseSectionPage implements OnDestroy { | ||||
|      */ | ||||
|     protected loadData(refresh?: boolean): Promise<any> { | ||||
|         // 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 = []; | ||||
|             let promise; | ||||
| 
 | ||||
|             this.course = course; | ||||
|             if (course) { | ||||
|                 this.course = course; | ||||
|             } | ||||
| 
 | ||||
|             // Get the completion status.
 | ||||
|             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. | ||||
|      * For perfomance reasons, do NOT call WebServices in here, call them in shouldDisplayForCourse. | ||||
|      * | ||||
|      * @param {number} courseId The course ID. | ||||
|      * @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>; | ||||
| 
 | ||||
|     /** | ||||
|      * 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. | ||||
|      * | ||||
| @ -91,12 +79,6 @@ export interface CoreCourseOptionsHandlerData { | ||||
|      */ | ||||
|     title: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Name of the icon to display for the handler. | ||||
|      * @type {string} | ||||
|      */ | ||||
|     icon: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Class to add to the displayed handler. | ||||
|      * @type {string} | ||||
| @ -104,11 +86,10 @@ export interface CoreCourseOptionsHandlerData { | ||||
|     class?: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Action to perform when the handler is clicked. | ||||
|      * | ||||
|      * @param {any} course The course. | ||||
|      * 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. | ||||
|      */ | ||||
|     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.
 | ||||
|             return this.getHandlersForAccess(course.id, refresh, accessData, course.navOptions, course.admOptions); | ||||
|         }).then(() => { | ||||
|             const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] = [], | ||||
|                 promises = []; | ||||
|             let promise; | ||||
|             const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] = []; | ||||
| 
 | ||||
|             this.coursesHandlers[course.id].enabledHandlers.forEach((handler) => { | ||||
|                 if (handler.shouldDisplayForCourse) { | ||||
|                     promise = Promise.resolve(handler.shouldDisplayForCourse( | ||||
|                         course.id, accessData, course.navOptions, course.admOptions)); | ||||
|                 } else { | ||||
|                     // 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; | ||||
|                 handlersToDisplay.push({ | ||||
|                     data: handler.getDisplayData(course), | ||||
|                     priority: handler.priority, | ||||
|                     prefetch: handler.prefetch | ||||
|                 }); | ||||
| 
 | ||||
|                 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} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. | ||||
|      * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @return {Promise}             Resolved when updated. | ||||
|      * @protected | ||||
|      * @return {Promise<any>} Resolved when updated. | ||||
|      */ | ||||
|     updateHandlersForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): Promise<any> { | ||||
|         const promises = [], | ||||
|  | ||||
| @ -10,7 +10,7 @@ | ||||
|     <core-loading [hideUntil]="dataLoaded"> | ||||
| 
 | ||||
|         <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> | ||||
|                 <h2><core-format-text [text]="course.fullname"></core-format-text></h2> | ||||
|                 <p *ngIf="course.categoryname">{{course.categoryname}}</p> | ||||
| @ -41,24 +41,15 @@ | ||||
|             <ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled"> | ||||
|                 <p>{{ 'core.courses.notenrollable' | translate }}</p> | ||||
|             </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-spinner *ngIf="prefetchCourseData.prefetchCourseIcon == 'spinner'" item-start></ion-spinner> | ||||
|                 <h2>{{ 'core.course.downloadcourse' | translate }}</h2> | ||||
|             </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> | ||||
|                 <h2>{{ 'core.course.contents' | translate }}</h2> | ||||
|             </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> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
|  | ||||
| @ -36,8 +36,7 @@ import { CoreCourseHelperProvider } from '../../../course/providers/helper'; | ||||
| export class CoreCoursesCoursePreviewPage implements OnDestroy { | ||||
|     course: any; | ||||
|     isEnrolled: boolean; | ||||
|     handlersShouldBeShown = true; | ||||
|     handlersLoaded: boolean; | ||||
|     canAccessCourse = true; | ||||
|     component = 'CoreCoursesCoursePreview'; | ||||
|     selfEnrolInstances: any[] = []; | ||||
|     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.
 | ||||
|                 this.course.fullname = course.fullname || this.course.fullname; | ||||
|                 this.course.summary = course.summary || this.course.summary; | ||||
| 
 | ||||
|                 return this.loadCourseHandlers(refresh, false); | ||||
|                 this.canAccessCourse = true; | ||||
|             }).catch(() => { | ||||
|                 // The user is not an admin/manager. Check if we can provide guest access to the course.
 | ||||
|                 return this.canAccessAsGuest().then((passwordRequired) => { | ||||
|                     if (!passwordRequired) { | ||||
|                         return this.loadCourseHandlers(refresh, true); | ||||
|                         this.canAccessCourse = true; | ||||
|                     } else { | ||||
|                         return Promise.reject(null); | ||||
|                         this.canAccessCourse = false; | ||||
|                     } | ||||
|                 }).catch(() => { | ||||
|                     this.course._handlers = []; | ||||
|                     this.handlersShouldBeShown = false; | ||||
|                     this.canAccessCourse = false; | ||||
|                 }); | ||||
|             }); | ||||
|         }).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. | ||||
|      */ | ||||
|     openCourse(): void { | ||||
|         if (!this.handlersShouldBeShown) { | ||||
|         if (!this.canAccessCourse) { | ||||
|             // Course cannot be opened.
 | ||||
|             return; | ||||
|         } | ||||
| @ -435,8 +418,7 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy { | ||||
|      * Prefetch the course. | ||||
|      */ | ||||
|     prefetchCourse(): void { | ||||
|         this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course, undefined, this.course._handlers) | ||||
|                 .catch((error) => { | ||||
|         this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course).catch((error) => { | ||||
|             if (!this.pageDestroyed) { | ||||
|                 this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); | ||||
|             } | ||||
|  | ||||
| @ -16,21 +16,33 @@ import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { IonicModule } from 'ionic-angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreUserParticipantsComponent } from './participants/participants'; | ||||
| 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({ | ||||
|     declarations: [ | ||||
|         CoreUserParticipantsComponent, | ||||
|         CoreUserProfileFieldComponent | ||||
|     ], | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
|         IonicModule, | ||||
|         TranslateModule.forChild(), | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CorePipesModule | ||||
|     ], | ||||
|     providers: [ | ||||
|     ], | ||||
|     exports: [ | ||||
|         CoreUserParticipantsComponent, | ||||
|         CoreUserProfileFieldComponent | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         CoreUserParticipantsComponent | ||||
|     ] | ||||
| }) | ||||
| export class CoreUserComponentsModule {} | ||||
|  | ||||
							
								
								
									
										25
									
								
								src/core/user/components/participants/participants.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/core/user/components/participants/participants.html
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||
							
								
								
									
										103
									
								
								src/core/user/components/participants/participants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/core/user/components/participants/participants.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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. --> | ||||
| <ng-template #userProfileField></ng-template> | ||||
| <core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component> | ||||
|  | ||||
| @ -12,8 +12,7 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, OnInit } from '@angular/core'; | ||||
| import { CoreLoggerProvider } from '../../../../providers/logger'; | ||||
| import { Component, Input, OnInit } from '@angular/core'; | ||||
| import { CoreUserProfileFieldDelegate } from '../../providers/user-profile-field-delegate'; | ||||
| 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() 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.
 | ||||
|     @ViewChild('userProfileField', { read: ViewContainerRef }) set userProfileField(el: ViewContainerRef) { | ||||
|         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; | ||||
|         } | ||||
|     } | ||||
|     componentClass: any; // The class of the component to render.
 | ||||
|     data: any = {}; // Data to pass to the component.
 | ||||
| 
 | ||||
|     protected logger; | ||||
| 
 | ||||
|     // 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'); | ||||
|     } | ||||
|     constructor(private ufDelegate: CoreUserProfileFieldDelegate, private utilsProvider: CoreUtilsProvider) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.createComponent(this.ufDelegate.getComponent(this.field, this.signup), this.fieldContainer); | ||||
|     } | ||||
|         this.componentClass = this.ufDelegate.getComponent(this.field, this.signup); | ||||
| 
 | ||||
|     /** | ||||
|      * Create a component, add it to a container and set the input data. | ||||
|      * | ||||
|      * @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(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; | ||||
|         this.data.field = this.field; | ||||
|         this.data.edit = this.utilsProvider.isTrueOrOne(this.edit); | ||||
|         if (this.edit) { | ||||
|             this.data.signup = this.utilsProvider.isTrueOrOne(this.signup); | ||||
|             this.data.disabled = this.utilsProvider.isTrueOrOne(this.field.locked); | ||||
|             this.data.form = this.form; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -15,6 +15,8 @@ | ||||
|     "lastname": "Surname", | ||||
|     "manager": "Manager", | ||||
|     "newpicture": "New picture", | ||||
|     "noparticipants": "No participants found for this course.", | ||||
|     "participants": "Participants", | ||||
|     "phone1": "Phone", | ||||
|     "phone2": "Mobile phone", | ||||
|     "roles": "Roles", | ||||
|  | ||||
							
								
								
									
										6
									
								
								src/core/user/pages/participants/participants.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/core/user/pages/participants/participants.html
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||
							
								
								
									
										31
									
								
								src/core/user/pages/participants/participants.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/core/user/pages/participants/participants.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 {} | ||||
							
								
								
									
										32
									
								
								src/core/user/pages/participants/participants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/core/user/pages/participants/participants.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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-row no-padding align-items-center text-center> | ||||
|                     <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> | ||||
|                             <p>{{comHandler.title | translate}}</p> | ||||
|                         </a> | ||||
| @ -37,7 +37,7 @@ | ||||
|                 </ion-row> | ||||
|             </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> | ||||
|                 <h2>{{ 'core.user.details' | translate }}</h2> | ||||
|             </a> | ||||
| @ -45,13 +45,13 @@ | ||||
|                 <ion-spinner></ion-spinner> | ||||
|             </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> | ||||
|                 <h2>{{ npHandler.title | translate }}</h2> | ||||
|             </a> | ||||
| 
 | ||||
|             <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> | ||||
|                     {{ actHandler.title | translate }} | ||||
|                     <ion-spinner *ngIf="actHandler.spinner" item-end></ion-spinner> | ||||
|  | ||||
| @ -12,8 +12,8 @@ | ||||
| // 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'; | ||||
| import { Component, Optional } from '@angular/core'; | ||||
| import { IonicPage, NavParams, NavController } from 'ionic-angular'; | ||||
| import { CoreUserProvider } from '../../providers/user'; | ||||
| import { CoreUserHelperProvider } from '../../providers/helper'; | ||||
| import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; | ||||
| @ -23,7 +23,8 @@ import { CoreEventsProvider } from '../../../../providers/events'; | ||||
| import { CoreSitesProvider } from '../../../../providers/sites'; | ||||
| import { CoreMimetypeUtilsProvider } from '../../../../providers/utils/mimetype'; | ||||
| 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. | ||||
| @ -45,15 +46,16 @@ export class CoreUserProfilePage { | ||||
|     title: string; | ||||
|     isDeleted = false; | ||||
|     canChangeProfilePicture = false; | ||||
|     actionHandlers = []; | ||||
|     newPageHandlers = []; | ||||
|     communicationHandlers = []; | ||||
|     actionHandlers: CoreUserProfileHandlerData[] = []; | ||||
|     newPageHandlers: CoreUserProfileHandlerData[] = []; | ||||
|     communicationHandlers: CoreUserProfileHandlerData[] = []; | ||||
| 
 | ||||
|     constructor(navParams: NavParams, private userProvider: CoreUserProvider, private userHelper: CoreUserHelperProvider, | ||||
|             private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private eventsProvider: CoreEventsProvider, | ||||
|             private coursesProvider: CoreCoursesProvider, private sitesProvider: CoreSitesProvider, | ||||
|             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.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. | ||||
|      */ | ||||
|  | ||||
							
								
								
									
										92
									
								
								src/core/user/providers/course-option-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/core/user/providers/course-option-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										74
									
								
								src/core/user/providers/participants-link-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/core/user/providers/participants-link-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { NavController } from 'ionic-angular'; | ||||
| import { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate'; | ||||
| import { CoreCoursesProvider } from '../../../core/courses/providers/courses'; | ||||
| import { CoreLoggerProvider } from '../../../providers/logger'; | ||||
| import { CoreSitesProvider } from '../../../providers/sites'; | ||||
| import { CoreEventsProvider } from '../../../providers/events'; | ||||
| 
 | ||||
| /** | ||||
|  * Interface that all user profile handlers must implement. | ||||
|  */ | ||||
| export interface CoreUserProfileHandler extends CoreDelegateHandler { | ||||
|     /** | ||||
|      * The highest priority is displayed first. | ||||
| @ -55,6 +59,9 @@ export interface CoreUserProfileHandler extends CoreDelegateHandler { | ||||
|     getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Data needed to render a user profile handler. It's returned by the handler. | ||||
|  */ | ||||
| export interface CoreUserProfileHandlerData { | ||||
|     /** | ||||
|      * Title to display. | ||||
| @ -88,12 +95,36 @@ export interface CoreUserProfileHandlerData { | ||||
| 
 | ||||
|     /** | ||||
|      * Action to do when clicked. | ||||
|      * @param  {any}    $event | ||||
|      * @param  {any}     user       User object. | ||||
|      * @param  {number}  courseId   Course ID where to show. | ||||
|      * @return {any}        Action to be done. | ||||
|      * | ||||
|      * @param {Event} event Click event. | ||||
|      * @param {NavController} Nav controller to use to navigate. | ||||
|      * @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 {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> { | ||||
|         const handlers = [], | ||||
|     getProfileHandlersFor(user: any, courseId: number): Promise<CoreUserProfileHandlerToDisplay[]> { | ||||
|         const handlers: CoreUserProfileHandlerToDisplay[] = [], | ||||
|             promises = []; | ||||
| 
 | ||||
|         // Retrieve course options forcing cache.
 | ||||
|  | ||||
| @ -60,9 +60,9 @@ export class CoreUserProfileMailHandler implements CoreUserProfileHandler { | ||||
|             icon: 'mail', | ||||
|             title: 'core.user.sendemail', | ||||
|             class: 'core-user-profile-mail', | ||||
|             action: ($event, user, courseId): void => { | ||||
|                 $event.preventDefault(); | ||||
|                 $event.stopPropagation(); | ||||
|             action: (event, navCtrl, user, courseId): void => { | ||||
|                 event.preventDefault(); | ||||
|                 event.stopPropagation(); | ||||
|                 window.open('mailto:' + user.email, '_blank'); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @ -23,6 +23,7 @@ import { CoreUtilsProvider } from '../../../providers/utils/utils'; | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreUserProvider { | ||||
|     static PARTICIPANTS_LIST_LIMIT = 50; // Max of participants to retrieve in each WS call.
 | ||||
|     static PROFILE_REFRESHED = 'CoreUserProfileRefreshed'; | ||||
|     static PROFILE_PICTURE_UPDATED = 'CoreUserProfilePictureUpdated'; | ||||
|     protected ROOT_CACHE_KEY = 'mmUser:'; | ||||
| @ -106,6 +107,59 @@ export class CoreUserProvider { | ||||
|         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. | ||||
|      * | ||||
| @ -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. | ||||
|      * | ||||
| @ -248,6 +355,17 @@ export class CoreUserProvider { | ||||
|         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. | ||||
|      * | ||||
| @ -268,4 +386,23 @@ export class CoreUserProvider { | ||||
|             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 { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; | ||||
| 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({ | ||||
|     declarations: [ | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreUserComponentsModule | ||||
|     ], | ||||
|     providers: [ | ||||
|         CoreUserDelegate, | ||||
| @ -34,15 +39,22 @@ import { CoreUserProfileLinkHandler } from './providers/user-link-handler'; | ||||
|         CoreUserProfileMailHandler, | ||||
|         CoreUserProvider, | ||||
|         CoreUserHelperProvider, | ||||
|         CoreUserProfileLinkHandler | ||||
|         CoreUserProfileLinkHandler, | ||||
|         CoreUserParticipantsCourseOptionHandler, | ||||
|         CoreUserParticipantsLinkHandler | ||||
|     ] | ||||
| }) | ||||
| export class CoreUserModule { | ||||
|     constructor(userDelegate: CoreUserDelegate, userProfileMailHandler: CoreUserProfileMailHandler, | ||||
|             eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, userProvider: CoreUserProvider, | ||||
|             contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler) { | ||||
|             contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler, | ||||
|             courseOptionHandler: CoreUserParticipantsCourseOptionHandler, linkHandler: CoreUserParticipantsLinkHandler, | ||||
|             courseOptionsDelegate: CoreCourseOptionsDelegate) { | ||||
| 
 | ||||
|         userDelegate.registerHandler(userProfileMailHandler); | ||||
|         courseOptionsDelegate.registerHandler(courseOptionHandler); | ||||
|         contentLinksDelegate.registerHandler(userLinkHandler); | ||||
|         contentLinksDelegate.registerHandler(linkHandler); | ||||
| 
 | ||||
|         eventsProvider.on(CoreEventsProvider.USER_DELETED, (data) => { | ||||
|             // Search for userid in params.
 | ||||
|  | ||||
| @ -96,6 +96,7 @@ | ||||
|     "info": "Info", | ||||
|     "ios": "iOS", | ||||
|     "labelsep": ": ", | ||||
|     "lastaccess": "Last access", | ||||
|     "lastdownloaded": "Last downloaded", | ||||
|     "lastmodified": "Last modified", | ||||
|     "lastsync": "Last synchronization", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user