MOBILE-3594 core: Implement context menu component
This commit is contained in:
		
							parent
							
								
									add521a0e7
								
							
						
					
					
						commit
						1ffcf4c877
					
				| @ -32,9 +32,13 @@ import { CoreEmptyBoxComponent } from './empty-box/empty-box'; | |||||||
| import { CoreTabsComponent } from './tabs/tabs'; | import { CoreTabsComponent } from './tabs/tabs'; | ||||||
| import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading'; | import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading'; | ||||||
| import { CoreProgressBarComponent } from './progress-bar/progress-bar'; | import { CoreProgressBarComponent } from './progress-bar/progress-bar'; | ||||||
|  | import { CoreContextMenuComponent } from './context-menu/context-menu'; | ||||||
|  | import { CoreContextMenuItemComponent } from './context-menu/context-menu-item'; | ||||||
|  | import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover'; | ||||||
| 
 | 
 | ||||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | import { CoreDirectivesModule } from '@directives/directives.module'; | ||||||
| import { CorePipesModule } from '@pipes/pipes.module'; | import { CorePipesModule } from '@pipes/pipes.module'; | ||||||
|  | import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
| @ -53,6 +57,10 @@ import { CorePipesModule } from '@pipes/pipes.module'; | |||||||
|         CoreTabsComponent, |         CoreTabsComponent, | ||||||
|         CoreInfiniteLoadingComponent, |         CoreInfiniteLoadingComponent, | ||||||
|         CoreProgressBarComponent, |         CoreProgressBarComponent, | ||||||
|  |         CoreContextMenuComponent, | ||||||
|  |         CoreContextMenuItemComponent, | ||||||
|  |         CoreContextMenuPopoverComponent, | ||||||
|  |         CoreNavBarButtonsComponent, | ||||||
|     ], |     ], | ||||||
|     imports: [ |     imports: [ | ||||||
|         CommonModule, |         CommonModule, | ||||||
| @ -77,6 +85,10 @@ import { CorePipesModule } from '@pipes/pipes.module'; | |||||||
|         CoreTabsComponent, |         CoreTabsComponent, | ||||||
|         CoreInfiniteLoadingComponent, |         CoreInfiniteLoadingComponent, | ||||||
|         CoreProgressBarComponent, |         CoreProgressBarComponent, | ||||||
|  |         CoreContextMenuComponent, | ||||||
|  |         CoreContextMenuItemComponent, | ||||||
|  |         CoreContextMenuPopoverComponent, | ||||||
|  |         CoreNavBarButtonsComponent, | ||||||
|     ], |     ], | ||||||
| }) | }) | ||||||
| export class CoreComponentsModule {} | export class CoreComponentsModule {} | ||||||
|  | |||||||
							
								
								
									
										106
									
								
								src/core/components/context-menu/context-menu-item.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/core/components/context-menu/context-menu-item.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,106 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component, Input, Output, OnInit, OnDestroy, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; | ||||||
|  | import { CoreContextMenuComponent } from './context-menu'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This directive adds a item to the Context Menu popover. | ||||||
|  |  * | ||||||
|  |  * @description | ||||||
|  |  * This directive defines and item to be added to the popover generated in CoreContextMenu. | ||||||
|  |  * | ||||||
|  |  * It is required to place this tag inside a core-context-menu tag. | ||||||
|  |  * | ||||||
|  |  * <core-context-menu> | ||||||
|  |  *     <core-context-menu-item [hidden]="showGrid" [priority]="601" [content]="'core.layoutgrid' | translate" | ||||||
|  |  *         (action)="switchGrid()" [iconAction]="'apps'"></core-context-menu-item> | ||||||
|  |  * </core-context-menu> | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-context-menu-item', | ||||||
|  |     template: '', | ||||||
|  | }) | ||||||
|  | export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChanges { | ||||||
|  | 
 | ||||||
|  |     @Input() content?: string; // Content of the item.
 | ||||||
|  |     @Input() iconDescription?: string; // Name of the icon to be shown on the left side of the item.
 | ||||||
|  |     @Input() iconAction?: string; // Name of the icon to show on the right side of the item. Represents the action to do on click.
 | ||||||
|  |     // If is "spinner" an spinner will be shown.
 | ||||||
|  |     // If no icon or spinner is selected, no action or link will work.
 | ||||||
|  |     // If href but no iconAction is provided arrow-right will be used.
 | ||||||
|  |     @Input() iconSlash?: boolean; // Display a red slash over the icon.
 | ||||||
|  |     @Input() ariaDescription?: string; // Aria label to add to iconDescription.
 | ||||||
|  |     @Input() ariaAction?: string; // Aria label to add to iconAction. If not set, it will be equal to content.
 | ||||||
|  |     @Input() href?: string; // Link to go if no action provided.
 | ||||||
|  |     @Input() captureLink?: boolean | string; // Whether the link needs to be captured by the app.
 | ||||||
|  |     @Input() autoLogin?: string; // Whether the link needs to be opened using auto-login.
 | ||||||
|  |     @Input() closeOnClick = true; // Whether to close the popover when the item is clicked.
 | ||||||
|  |     @Input() priority?: number; // Used to sort items. The highest priority, the highest position.
 | ||||||
|  |     @Input() badge?: string; // A badge to show in the item.
 | ||||||
|  |     @Input() badgeClass?: number; // A class to set in the badge.
 | ||||||
|  |     @Input() hidden?: boolean; // Whether the item should be hidden.
 | ||||||
|  |     @Output() action?: EventEmitter<() => void>; // Will emit an event when the item clicked.
 | ||||||
|  |     @Output() onClosed?: EventEmitter<() => void>; // Will emit an event when the popover is closed because the item was clicked.
 | ||||||
|  | 
 | ||||||
|  |     protected hasAction = false; | ||||||
|  |     protected destroyed = false; | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         protected ctxtMenu: CoreContextMenuComponent, | ||||||
|  |     ) { | ||||||
|  |         this.action = new EventEmitter(); | ||||||
|  |         this.onClosed = new EventEmitter(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component being initialized. | ||||||
|  |      */ | ||||||
|  |     ngOnInit(): void { | ||||||
|  |         // Initialize values.
 | ||||||
|  |         this.priority = this.priority || 1; | ||||||
|  |         this.hasAction = !!this.action && this.action.observers.length > 0; | ||||||
|  |         this.ariaAction = this.ariaAction || this.content; | ||||||
|  | 
 | ||||||
|  |         if (this.hasAction) { | ||||||
|  |             this.href = ''; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Navigation help if href provided.
 | ||||||
|  |         this.captureLink = this.href && this.captureLink ? this.captureLink : false; | ||||||
|  |         this.autoLogin = this.autoLogin || 'check'; | ||||||
|  | 
 | ||||||
|  |         if (!this.destroyed) { | ||||||
|  |             this.ctxtMenu.addItem(this); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component destroyed. | ||||||
|  |      */ | ||||||
|  |     ngOnDestroy(): void { | ||||||
|  |         this.destroyed = true; | ||||||
|  |         this.ctxtMenu.removeItem(this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Detect changes on input properties. | ||||||
|  |      */ | ||||||
|  |     ngOnChanges(changes: { [name: string]: SimpleChange }): void { | ||||||
|  |         if (changes.hidden && !changes.hidden.firstChange) { | ||||||
|  |             this.ctxtMenu.itemsChanged(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | :host { | ||||||
|  |     ion-list { | ||||||
|  |         padding: 0; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										77
									
								
								src/core/components/context-menu/context-menu-popover.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/core/components/context-menu/context-menu-popover.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component } from '@angular/core'; | ||||||
|  | import { NavParams, PopoverController } from '@ionic/angular'; | ||||||
|  | import { CoreContextMenuItemComponent } from './context-menu-item'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Component to display a list of items received by param in a popover. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-context-menu-popover', | ||||||
|  |     templateUrl: 'core-context-menu-popover.html', | ||||||
|  |     styleUrls: ['context-menu-popover.scss'], | ||||||
|  | }) | ||||||
|  | export class CoreContextMenuPopoverComponent { | ||||||
|  | 
 | ||||||
|  |     title: string; | ||||||
|  |     uniqueId: string; | ||||||
|  |     items: CoreContextMenuItemComponent[]; | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         navParams: NavParams, | ||||||
|  |         protected popoverCtrl: PopoverController, | ||||||
|  |     ) { | ||||||
|  |         this.title = navParams.get('title'); | ||||||
|  |         this.items = navParams.get('items') || []; | ||||||
|  |         this.uniqueId = navParams.get('id'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Close the popover. | ||||||
|  |      */ | ||||||
|  |     closeMenu(item?: CoreContextMenuItemComponent): void { | ||||||
|  |         this.popoverCtrl.dismiss(item); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Function called when an item is clicked. | ||||||
|  |      * | ||||||
|  |      * @param event Click event. | ||||||
|  |      * @param item Item clicked. | ||||||
|  |      * @return Return true if success, false if error. | ||||||
|  |      */ | ||||||
|  |     itemClicked(event: Event, item: CoreContextMenuItemComponent): boolean { | ||||||
|  |         if (!!item.action && item.action.observers.length > 0) { | ||||||
|  |             event.preventDefault(); | ||||||
|  |             event.stopPropagation(); | ||||||
|  | 
 | ||||||
|  |             if (item.iconAction == 'spinner') { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (item.closeOnClick) { | ||||||
|  |                 this.closeMenu(item); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             item.action.emit(this.closeMenu.bind(this, item)); | ||||||
|  |         } else if (item.closeOnClick && (item.href || (!!item.onClosed && item.onClosed.observers.length > 0))) { | ||||||
|  |             this.closeMenu(item); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										213
									
								
								src/core/components/context-menu/context-menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								src/core/components/context-menu/context-menu.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,213 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core'; | ||||||
|  | import { Subject } from 'rxjs'; | ||||||
|  | import { auditTime } from 'rxjs/operators'; | ||||||
|  | import { PopoverController } from '@ionic/angular'; | ||||||
|  | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { Translate } from '@singletons/core.singletons'; | ||||||
|  | import { CoreContextMenuItemComponent } from './context-menu-item'; | ||||||
|  | import { CoreContextMenuPopoverComponent } from './context-menu-popover'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This component adds a button (usually in the navigation bar) that displays a context menu popover. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-context-menu', | ||||||
|  |     templateUrl: 'core-context-menu.html', | ||||||
|  | }) | ||||||
|  | export class CoreContextMenuComponent implements OnInit, OnDestroy { | ||||||
|  | 
 | ||||||
|  |     @Input() icon?: string; // Icon to be shown on the navigation bar. Default: Kebab menu icon.
 | ||||||
|  |     @Input() title?: string; // Text to be shown on the top of the popover.
 | ||||||
|  |     @Input('aria-label') ariaLabel?: string; // Aria label to be shown on the top of the popover.
 | ||||||
|  | 
 | ||||||
|  |     hideMenu = true; // It will be unhidden when items are added.
 | ||||||
|  |     expanded = false; | ||||||
|  |     protected items: CoreContextMenuItemComponent[] = []; | ||||||
|  |     protected itemsMovedToParent: CoreContextMenuItemComponent[] = []; | ||||||
|  |     protected itemsChangedStream: Subject<void>; // Stream to update the hideMenu boolean when items change.
 | ||||||
|  |     protected instanceId: string; | ||||||
|  |     protected parentContextMenu?: CoreContextMenuComponent; | ||||||
|  |     protected uniqueId: string; | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         protected popoverCtrl: PopoverController, | ||||||
|  |         elementRef: ElementRef, | ||||||
|  |     ) { | ||||||
|  |         // Create the stream and subscribe to it. We ignore successive changes during 250ms.
 | ||||||
|  |         this.itemsChangedStream = new Subject<void>(); | ||||||
|  |         this.itemsChangedStream.pipe(auditTime(250)); | ||||||
|  |         this.itemsChangedStream.subscribe(() => { | ||||||
|  |             // Hide the menu if all items are hidden.
 | ||||||
|  |             this.hideMenu = !this.items.some((item) => !item.hidden); | ||||||
|  | 
 | ||||||
|  |             // Sort the items by priority.
 | ||||||
|  |             this.items.sort((a, b) => (a.priority || 0) <= (b.priority || 0) ? 1 : -1); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // Calculate the unique ID.
 | ||||||
|  |         this.uniqueId = 'core-context-menu-' + CoreUtils.instance.getUniqueId('CoreContextMenuComponent'); | ||||||
|  | 
 | ||||||
|  |         this.instanceId = CoreDomUtils.instance.storeInstanceByElement(elementRef.nativeElement, this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component being initialized. | ||||||
|  |      */ | ||||||
|  |     ngOnInit(): void { | ||||||
|  |         this.icon = this.icon || 'ellipsis-vertical'; | ||||||
|  |         this.ariaLabel = this.ariaLabel || this.title || Translate.instance.instant('core.displayoptions'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add a context menu item. | ||||||
|  |      * | ||||||
|  |      * @param item The item to add. | ||||||
|  |      */ | ||||||
|  |     addItem(item: CoreContextMenuItemComponent): void { | ||||||
|  |         if (this.parentContextMenu) { | ||||||
|  |             // All items were moved to the "parent" menu. Add the item in there.
 | ||||||
|  |             this.parentContextMenu.addItem(item); | ||||||
|  | 
 | ||||||
|  |             if (this.itemsMovedToParent.indexOf(item) == -1) { | ||||||
|  |                 this.itemsMovedToParent.push(item); | ||||||
|  |             } | ||||||
|  |         } else if (this.items.indexOf(item) == -1) { | ||||||
|  |             this.items.push(item); | ||||||
|  |             this.itemsChanged(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Function called when the items change. | ||||||
|  |      */ | ||||||
|  |     itemsChanged(): void { | ||||||
|  |         if (this.parentContextMenu) { | ||||||
|  |             // All items were moved to the "parent" menu, call the function in there.
 | ||||||
|  |             this.parentContextMenu.itemsChanged(); | ||||||
|  |         } else { | ||||||
|  |             this.itemsChangedStream.next(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Merge the current context menu with the one passed as parameter. All the items in this menu will be moved to the | ||||||
|  |      * one passed as parameter. | ||||||
|  |      * | ||||||
|  |      * @param contextMenu The context menu where to move the items. | ||||||
|  |      */ | ||||||
|  |     mergeContextMenus(contextMenu: CoreContextMenuComponent): void { | ||||||
|  |         this.parentContextMenu = contextMenu; | ||||||
|  | 
 | ||||||
|  |         // Add all the items to the other menu.
 | ||||||
|  |         for (let i = 0; i < this.items.length; i++) { | ||||||
|  |             const item = this.items[i]; | ||||||
|  |             contextMenu.addItem(item); | ||||||
|  |             this.itemsMovedToParent.push(item); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Remove all items from the current menu.
 | ||||||
|  |         this.items = []; | ||||||
|  |         this.itemsChanged(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Remove an item from the context menu. | ||||||
|  |      * | ||||||
|  |      * @param item The item to remove. | ||||||
|  |      */ | ||||||
|  |     removeItem(item: CoreContextMenuItemComponent): void { | ||||||
|  |         if (this.parentContextMenu) { | ||||||
|  |             // All items were moved to the "parent" menu. Remove the item from there.
 | ||||||
|  |             this.parentContextMenu.removeItem(item); | ||||||
|  | 
 | ||||||
|  |             const index = this.itemsMovedToParent.indexOf(item); | ||||||
|  |             if (index >= 0) { | ||||||
|  |                 this.itemsMovedToParent.splice(index, 1); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             const index = this.items.indexOf(item); | ||||||
|  |             if (index >= 0) { | ||||||
|  |                 this.items.splice(index, 1); | ||||||
|  |             } | ||||||
|  |             this.itemsChanged(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Remove the items that were merged to a parent context menu. | ||||||
|  |      */ | ||||||
|  |     removeMergedItems(): void { | ||||||
|  |         if (this.parentContextMenu) { | ||||||
|  |             for (let i = 0; i < this.itemsMovedToParent.length; i++) { | ||||||
|  |                 this.parentContextMenu.removeItem(this.itemsMovedToParent[i]); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Restore the items that were merged to a parent context menu. | ||||||
|  |      */ | ||||||
|  |     restoreMergedItems(): void { | ||||||
|  |         if (this.parentContextMenu) { | ||||||
|  |             for (let i = 0; i < this.itemsMovedToParent.length; i++) { | ||||||
|  |                 this.parentContextMenu.addItem(this.itemsMovedToParent[i]); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show the context menu. | ||||||
|  |      * | ||||||
|  |      * @param event Event. | ||||||
|  |      */ | ||||||
|  |     async showContextMenu(event: MouseEvent): Promise<void> { | ||||||
|  |         if (!this.expanded) { | ||||||
|  |             const popover = await this.popoverCtrl.create( | ||||||
|  |                 { | ||||||
|  |                     event, | ||||||
|  |                     component: CoreContextMenuPopoverComponent, | ||||||
|  |                     componentProps: { | ||||||
|  |                         title: this.title, | ||||||
|  |                         items: this.items, | ||||||
|  |                     }, | ||||||
|  |                     showBackdrop: true, | ||||||
|  |                     id: this.uniqueId, | ||||||
|  |                 }, | ||||||
|  |             ); | ||||||
|  |             await popover.present(); | ||||||
|  |             this.expanded = true; | ||||||
|  | 
 | ||||||
|  |             const data = await popover.onDidDismiss<CoreContextMenuItemComponent>(); | ||||||
|  |             this.expanded = false; | ||||||
|  | 
 | ||||||
|  |             if (data.data) { | ||||||
|  |                 data.data.onClosed?.emit(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component destroyed. | ||||||
|  |      */ | ||||||
|  |     ngOnDestroy(): void { | ||||||
|  |         CoreDomUtils.instance.removeInstanceById(this.instanceId); | ||||||
|  |         this.removeMergedItems(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,15 @@ | |||||||
|  | <ion-list [id]="uniqueId" role="menu"> | ||||||
|  |     <ion-list-header *ngIf="title">{{title}}</ion-list-header> | ||||||
|  |     <ion-item class="ion-text-wrap" *ngFor="let item of items" core-link [capture]="item.captureLink" [autoLogin]="item.autoLogin" | ||||||
|  |     [href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction" [hidden]="item.hidden" | ||||||
|  |     [detail]="(item.href && !item.iconAction) || null" role="menuitem"> | ||||||
|  |         <ion-icon *ngIf="item.iconDescription" [name]="item.iconDescription" [attr.aria-label]="item.ariaDescription" slot="start"> | ||||||
|  |         </ion-icon> | ||||||
|  |         <core-format-text [clean]="true" [text]="item.content" [filter]="false"></core-format-text> | ||||||
|  |         <ion-icon *ngIf="(item.href || item.action) && item.iconAction && item.iconAction != 'spinner'" [name]="item.iconAction" | ||||||
|  |             [class.icon-slash]="item.iconSlash" slot="end"> | ||||||
|  |         </ion-icon> | ||||||
|  |         <ion-spinner *ngIf="(item.href || item.action) && item.iconAction == 'spinner'" slot="end"></ion-spinner> | ||||||
|  |         <ion-badge class="{{item.badgeClass}}" slot="end" *ngIf="item.badge">{{item.badge}}</ion-badge> | ||||||
|  |     </ion-item> | ||||||
|  | </ion-list> | ||||||
							
								
								
									
										6
									
								
								src/core/components/context-menu/core-context-menu.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/core/components/context-menu/core-context-menu.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | <ion-button [hidden]="hideMenu" fill="clear" [attr.aria-label]="ariaLabel" | ||||||
|  | (click)="showContextMenu($event)" aria-haspopup="true" [attr.aria-expanded]="expanded" [attr.aria-controls]="uniqueId"> | ||||||
|  |     <ion-icon [name]="icon" slot="icon-only"> | ||||||
|  |     </ion-icon> | ||||||
|  | </ion-button> | ||||||
|  | <ng-content></ng-content> | ||||||
							
								
								
									
										3
									
								
								src/core/components/navbar-buttons/navbar-buttons.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/core/components/navbar-buttons/navbar-buttons.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | :host { | ||||||
|  |     display: none !important; | ||||||
|  | } | ||||||
							
								
								
									
										269
									
								
								src/core/components/navbar-buttons/navbar-buttons.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								src/core/components/navbar-buttons/navbar-buttons.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,269 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core'; | ||||||
|  | import { CoreLogger } from '@singletons/logger'; | ||||||
|  | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { CoreContextMenuComponent } from '../context-menu/context-menu'; | ||||||
|  | 
 | ||||||
|  | const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 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. | ||||||
|  |  * | ||||||
|  |  * If this component has a "prepend" attribute, the buttons will be added before other existing buttons in the header. | ||||||
|  |  * | ||||||
|  |  * You can use the [hidden] input to hide all the inner buttons if a certain condition is met. | ||||||
|  |  * | ||||||
|  |  * IMPORTANT: Do not use *ngIf in the buttons inside this component, it can cause problems. Please use [hidden] instead. | ||||||
|  |  * | ||||||
|  |  * Example usage: | ||||||
|  |  * | ||||||
|  |  * <core-navbar-buttons slot="end"> | ||||||
|  |  *     <ion-button [hidden]="!buttonShown" [attr.aria-label]="Do something" (click)="action()"> | ||||||
|  |  *         <ion-icon name="funnel" slot="icon-only"></ion-icon> | ||||||
|  |  *     </ion-button> | ||||||
|  |  * </core-navbar-buttons> | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-navbar-buttons', | ||||||
|  |     template: '<ng-content></ng-content>', | ||||||
|  |     styleUrls: ['navbar-buttons.scss'], | ||||||
|  | }) | ||||||
|  | export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { | ||||||
|  | 
 | ||||||
|  |     // If the hidden input is true, hide all buttons.
 | ||||||
|  |     // eslint-disable-next-line @angular-eslint/no-input-rename
 | ||||||
|  |     @Input('hidden') set hidden(value: boolean) { | ||||||
|  |         if (typeof value == 'string' && value == '') { | ||||||
|  |             value = true; | ||||||
|  |         } | ||||||
|  |         this.allButtonsHidden = value; | ||||||
|  |         this.showHideAllElements(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected element: HTMLElement; | ||||||
|  |     protected allButtonsHidden = false; | ||||||
|  |     protected forceHidden = false; | ||||||
|  |     protected logger: CoreLogger; | ||||||
|  |     protected movedChildren?: Node[]; | ||||||
|  |     protected instanceId: string; | ||||||
|  |     protected mergedContextMenu?: CoreContextMenuComponent; | ||||||
|  | 
 | ||||||
|  |     constructor(element: ElementRef) { | ||||||
|  |         this.element = element.nativeElement; | ||||||
|  |         this.logger = CoreLogger.getInstance('CoreNavBarButtonsComponent'); | ||||||
|  |         this.instanceId = CoreDomUtils.instance.storeInstanceByElement(this.element, this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component being initialized. | ||||||
|  |      */ | ||||||
|  |     async ngOnInit(): Promise<void> { | ||||||
|  |         try { | ||||||
|  |             const header = await this.searchHeader(); | ||||||
|  |             if (header) { | ||||||
|  |                 // Search the right buttons container (start, end or any).
 | ||||||
|  |                 let selector = 'ion-buttons'; | ||||||
|  | 
 | ||||||
|  |                 let slot = this.element.getAttribute('slot'); | ||||||
|  |                 // Take the slot from the parent if it has.
 | ||||||
|  |                 if (!slot && this.element.parentElement) { | ||||||
|  |                     slot = this.element.parentElement.getAttribute('slot'); | ||||||
|  |                 } | ||||||
|  |                 if (slot) { | ||||||
|  |                     selector += '[slot="' + slot + '"]'; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 const buttonsContainer = <HTMLElement> header.querySelector(selector); | ||||||
|  |                 if (buttonsContainer) { | ||||||
|  |                     this.mergeContextMenus(buttonsContainer); | ||||||
|  | 
 | ||||||
|  |                     const prepend = this.element.hasAttribute('prepend'); | ||||||
|  | 
 | ||||||
|  |                     this.movedChildren = CoreDomUtils.instance.moveChildren(this.element, buttonsContainer, prepend); | ||||||
|  |                     this.showHideAllElements(); | ||||||
|  | 
 | ||||||
|  |                 } else { | ||||||
|  |                     this.logger.warn('The header was found, but it didn\'t have the right ion-buttons.', selector); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (error) { | ||||||
|  |             // Header not found.
 | ||||||
|  |             this.logger.warn(error); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Force or unforce hiding all buttons. If this is true, it will override the "hidden" input. | ||||||
|  |      * | ||||||
|  |      * @param value The value to set. | ||||||
|  |      */ | ||||||
|  |     forceHide(value: boolean): void { | ||||||
|  |         this.forceHidden = value; | ||||||
|  | 
 | ||||||
|  |         this.showHideAllElements(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * If both button containers have a context menu, merge them into a single one. | ||||||
|  |      * | ||||||
|  |      * @param buttonsContainer The container where the buttons will be moved. | ||||||
|  |      * @todo | ||||||
|  |      */ | ||||||
|  |     protected mergeContextMenus(buttonsContainer: HTMLElement): void { | ||||||
|  |         // Check if both button containers have a context menu.
 | ||||||
|  |         const mainContextMenu = buttonsContainer.querySelector('core-context-menu'); | ||||||
|  |         if (!mainContextMenu) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const secondaryContextMenu = this.element.querySelector('core-context-menu'); | ||||||
|  |         if (!secondaryContextMenu) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Both containers have a context menu. Merge them to prevent having 2 menus at the same time.
 | ||||||
|  |         const mainContextMenuInstance: CoreContextMenuComponent = CoreDomUtils.instance.getInstanceByElement(mainContextMenu); | ||||||
|  |         const secondaryContextMenuInstance: CoreContextMenuComponent = | ||||||
|  |             CoreDomUtils.instance.getInstanceByElement(secondaryContextMenu); | ||||||
|  | 
 | ||||||
|  |         // Check that both context menus belong to the same core-tab. We shouldn't merge menus from different tabs.
 | ||||||
|  |         if (mainContextMenuInstance && secondaryContextMenuInstance) { | ||||||
|  |             this.mergedContextMenu = secondaryContextMenuInstance; | ||||||
|  | 
 | ||||||
|  |             this.mergedContextMenu.mergeContextMenus(mainContextMenuInstance); | ||||||
|  | 
 | ||||||
|  |             // Remove the empty context menu from the DOM.
 | ||||||
|  |             secondaryContextMenu.parentElement?.removeChild(secondaryContextMenu); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Search the ion-header where the buttons should be added. | ||||||
|  |      * | ||||||
|  |      * @param retries Number of retries so far. | ||||||
|  |      * @return Promise resolved with the header element. | ||||||
|  |      */ | ||||||
|  |     protected async searchHeader(retries: number = 0): Promise<HTMLElement> { | ||||||
|  |         let parentPage: HTMLElement = this.element; | ||||||
|  | 
 | ||||||
|  |         while (parentPage) { | ||||||
|  |             if (!parentPage.parentElement) { | ||||||
|  |                 // No parent, stop.
 | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Get the next parent page.
 | ||||||
|  |             parentPage = <HTMLElement> CoreDomUtils.instance.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 && getComputedStyle(header, null).display != 'none') { | ||||||
|  |                     return header; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Header not found.
 | ||||||
|  |         if (retries < 5) { | ||||||
|  |             // If the component or any of its parent is inside a ng-content or similar it can be detached when it's initialized.
 | ||||||
|  |             // Try again after a while.
 | ||||||
|  |             return new Promise((resolve, reject): void => { | ||||||
|  |                 setTimeout(() => { | ||||||
|  |                     // eslint-disable-next-line promise/catch-or-return
 | ||||||
|  |                     this.searchHeader(retries + 1).then(resolve, reject); | ||||||
|  |                 }, 200); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // We've waited enough time, reject.
 | ||||||
|  |         throw Error('Header not found.'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Search ion-header inside a page. The header should be a direct child. | ||||||
|  |      * | ||||||
|  |      * @param page Page to search in. | ||||||
|  |      * @return Header element. Undefined if not found. | ||||||
|  |      */ | ||||||
|  |     protected searchHeaderInPage(page: HTMLElement): HTMLElement | undefined { | ||||||
|  |         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 all the elements. | ||||||
|  |      */ | ||||||
|  |     protected showHideAllElements(): void { | ||||||
|  |         // Show or hide all moved children.
 | ||||||
|  |         if (this.movedChildren) { | ||||||
|  |             this.movedChildren.forEach((child: Node) => { | ||||||
|  |                 this.showHideElement(child); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Show or hide all the context menu items that were merged to another context menu.
 | ||||||
|  |         if (this.mergedContextMenu) { | ||||||
|  |             if (this.forceHidden || this.allButtonsHidden) { | ||||||
|  |                 this.mergedContextMenu.removeMergedItems(); | ||||||
|  |             } else { | ||||||
|  |                 this.mergedContextMenu.restoreMergedItems(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show or hide an element. | ||||||
|  |      * | ||||||
|  |      * @param element Element to show or hide. | ||||||
|  |      */ | ||||||
|  |     protected showHideElement(element: Node): void { | ||||||
|  |         // Check if it's an HTML Element
 | ||||||
|  |         if (element instanceof Element) { | ||||||
|  |             element.classList.toggle(BUTTON_HIDDEN_CLASS, !!this.forceHidden || !!this.allButtonsHidden); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component destroyed. | ||||||
|  |      */ | ||||||
|  |     ngOnDestroy(): void { | ||||||
|  |         CoreDomUtils.instance.removeInstanceById(this.instanceId); | ||||||
|  | 
 | ||||||
|  |         // This component was destroyed, remove all the buttons that were moved.
 | ||||||
|  |         // The buttons can be moved outside of the current page, that's why we need to manually destroy them.
 | ||||||
|  |         // There's no need to destroy context menu items that were merged because they weren't moved from their DOM position.
 | ||||||
|  |         if (this.movedChildren) { | ||||||
|  |             this.movedChildren.forEach((child) => { | ||||||
|  |                 if (child.parentElement) { | ||||||
|  |                     child.parentElement.removeChild(child); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (this.mergedContextMenu) { | ||||||
|  |             this.mergedContextMenu.removeMergedItems(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -8,9 +8,15 @@ | |||||||
|             <core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0"></core-format-text> |             <core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0"></core-format-text> | ||||||
|             <img src="assets/img/login_logo.png" class="core-header-logo" [alt]="siteName"> |             <img src="assets/img/login_logo.png" class="core-header-logo" [alt]="siteName"> | ||||||
|         </ion-title> |         </ion-title> | ||||||
| 
 |  | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <!-- @todo --> |             <ion-button *ngIf="searchEnabled" (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate"> | ||||||
|  |                 <ion-icon name="fas-search" slot="icon-only"></ion-icon> | ||||||
|  |             </ion-button> | ||||||
|  |             <core-context-menu> | ||||||
|  |                 <core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="500" | ||||||
|  |                 [content]="'addon.storagemanager.managestorage' | translate" | ||||||
|  |                 (action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item> | ||||||
|  |             </core-context-menu> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
|  | |||||||
| @ -12,10 +12,14 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { CoreSites } from '@services/sites'; |  | ||||||
| import { Component, OnInit } from '@angular/core'; | import { Component, OnInit } from '@angular/core'; | ||||||
|  | import { NavController } from '@ionic/angular'; | ||||||
| import { Subscription } from 'rxjs'; | import { Subscription } from 'rxjs'; | ||||||
|  | 
 | ||||||
|  | import { CoreSites } from '@services/sites'; | ||||||
| import { CoreHomeDelegate, CoreHomeHandlerToDisplay } from '../../services/home.delegate'; | import { CoreHomeDelegate, CoreHomeHandlerToDisplay } from '../../services/home.delegate'; | ||||||
|  | import { CoreCourses } from '@features/courses/services/courses'; | ||||||
|  | import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Page that displays the Home. |  * Page that displays the Home. | ||||||
| @ -31,11 +35,16 @@ export class CoreHomePage implements OnInit { | |||||||
|     tabs: CoreHomeHandlerToDisplay[] = []; |     tabs: CoreHomeHandlerToDisplay[] = []; | ||||||
|     loaded = false; |     loaded = false; | ||||||
|     selectedTab?: number; |     selectedTab?: number; | ||||||
|  |     searchEnabled = false; | ||||||
|  |     downloadCourseEnabled = false; | ||||||
|  |     downloadCoursesEnabled = false; | ||||||
| 
 | 
 | ||||||
|     protected subscription?: Subscription; |     protected subscription?: Subscription; | ||||||
|  |     protected updateSiteObserver?: CoreEventObserver; | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         protected homeDelegate: CoreHomeDelegate, |         protected homeDelegate: CoreHomeDelegate, | ||||||
|  |         protected navCtrl: NavController, | ||||||
|     ) { |     ) { | ||||||
|         this.loadSiteName(); |         this.loadSiteName(); | ||||||
|     } |     } | ||||||
| @ -44,9 +53,22 @@ export class CoreHomePage implements OnInit { | |||||||
|      * Initialize the component. |      * Initialize the component. | ||||||
|      */ |      */ | ||||||
|     ngOnInit(): void { |     ngOnInit(): void { | ||||||
|  |         this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite(); | ||||||
|  |         this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); | ||||||
|  |         this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); | ||||||
|  | 
 | ||||||
|         this.subscription = this.homeDelegate.getHandlersObservable().subscribe((handlers) => { |         this.subscription = this.homeDelegate.getHandlersObservable().subscribe((handlers) => { | ||||||
|             handlers && this.initHandlers(handlers); |             handlers && this.initHandlers(handlers); | ||||||
|         }); |         }); | ||||||
|  | 
 | ||||||
|  |         // Refresh the enabled flags if site is updated.
 | ||||||
|  |         this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { | ||||||
|  |             this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite(); | ||||||
|  |             this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); | ||||||
|  |             this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); | ||||||
|  | 
 | ||||||
|  |             this.loadSiteName(); | ||||||
|  |         }, CoreSites.instance.getCurrentSiteId()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -91,4 +113,18 @@ export class CoreHomePage implements OnInit { | |||||||
|         this.siteName = CoreSites.instance.getCurrentSite()!.getSiteName(); |         this.siteName = CoreSites.instance.getCurrentSite()!.getSiteName(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Open page to manage courses storage. | ||||||
|  |      */ | ||||||
|  |     manageCoursesStorage(): void { | ||||||
|  |         // @todo this.navCtrl.navigateForward(['/courses/storage']);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Go to search courses. | ||||||
|  |      */ | ||||||
|  |     openSearch(): void { | ||||||
|  |         this.navCtrl.navigateForward(['/courses/search']); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -5,10 +5,11 @@ | |||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title>{{ 'core.settings.spaceusage' | translate }}</ion-title> |         <ion-title>{{ 'core.settings.spaceusage' | translate }}</ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <!-- @todo <core-navbar-buttons></core-navbar-buttons>--> |             <core-navbar-buttons> | ||||||
|                 <ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate"> |                 <ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate"> | ||||||
|                     <ion-icon name="fas-info-circle" slot="icon-only"></ion-icon> |                     <ion-icon name="fas-info-circle" slot="icon-only"></ion-icon> | ||||||
|                 </ion-button> |                 </ion-button> | ||||||
|  |             </core-navbar-buttons> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
|  | |||||||
| @ -5,10 +5,11 @@ | |||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title>{{ 'core.settings.synchronization' | translate }}</ion-title> |         <ion-title>{{ 'core.settings.synchronization' | translate }}</ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <!-- @todo <core-navbar-buttons></core-navbar-buttons>--> |             <core-navbar-buttons> | ||||||
|                 <ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate"> |                 <ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate"> | ||||||
|                     <ion-icon name="fas-info-circle" slot="icon-only"></ion-icon> |                     <ion-icon name="fas-info-circle" slot="icon-only"></ion-icon> | ||||||
|                 </ion-button> |                 </ion-button> | ||||||
|  |             </core-navbar-buttons> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
|  | |||||||
| @ -10,6 +10,10 @@ ion-toolbar .in-toolbar.button-clear { | |||||||
|     --color: var(--ion-color-primary-contrast); |     --color: var(--ion-color-primary-contrast); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | ion-toolbar .core-navbar-button-hidden { | ||||||
|  |     display: none !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Ionic icon. | // Ionic icon. | ||||||
| ion-icon { | ion-icon { | ||||||
|     &.icon-slash::after, |     &.icon-slash::after, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user