MOBILE-3594 core: Implement context menu component
parent
add521a0e7
commit
1ffcf4c877
|
@ -32,9 +32,13 @@ import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
|||
import { CoreTabsComponent } from './tabs/tabs';
|
||||
import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading';
|
||||
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 { CorePipesModule } from '@pipes/pipes.module';
|
||||
import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -53,6 +57,10 @@ import { CorePipesModule } from '@pipes/pipes.module';
|
|||
CoreTabsComponent,
|
||||
CoreInfiniteLoadingComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreContextMenuComponent,
|
||||
CoreContextMenuItemComponent,
|
||||
CoreContextMenuPopoverComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -77,6 +85,10 @@ import { CorePipesModule } from '@pipes/pipes.module';
|
|||
CoreTabsComponent,
|
||||
CoreInfiniteLoadingComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreContextMenuComponent,
|
||||
CoreContextMenuItemComponent,
|
||||
CoreContextMenuPopoverComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
],
|
||||
})
|
||||
export class CoreComponentsModule {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
:host {
|
||||
display: none !important;
|
||||
}
|
|
@ -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>
|
||||
<img src="assets/img/login_logo.png" class="core-header-logo" [alt]="siteName">
|
||||
</ion-title>
|
||||
|
||||
<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-toolbar>
|
||||
</ion-header>
|
||||
|
|
|
@ -12,10 +12,14 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { CoreSites } from '@services/sites';
|
||||
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.
|
||||
|
@ -31,11 +35,16 @@ export class CoreHomePage implements OnInit {
|
|||
tabs: CoreHomeHandlerToDisplay[] = [];
|
||||
loaded = false;
|
||||
selectedTab?: number;
|
||||
searchEnabled = false;
|
||||
downloadCourseEnabled = false;
|
||||
downloadCoursesEnabled = false;
|
||||
|
||||
protected subscription?: Subscription;
|
||||
protected updateSiteObserver?: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
protected homeDelegate: CoreHomeDelegate,
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
this.loadSiteName();
|
||||
}
|
||||
|
@ -44,9 +53,22 @@ export class CoreHomePage implements OnInit {
|
|||
* Initialize the component.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
|
||||
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
|
||||
|
||||
this.subscription = this.homeDelegate.getHandlersObservable().subscribe((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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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-title>{{ 'core.settings.spaceusage' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<!-- @todo <core-navbar-buttons></core-navbar-buttons>-->
|
||||
<ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate">
|
||||
<ion-icon name="fas-info-circle" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<core-navbar-buttons>
|
||||
<ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate">
|
||||
<ion-icon name="fas-info-circle" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</core-navbar-buttons>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
</ion-buttons>
|
||||
<ion-title>{{ 'core.settings.synchronization' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<!-- @todo <core-navbar-buttons></core-navbar-buttons>-->
|
||||
<ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate">
|
||||
<ion-icon name="fas-info-circle" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<core-navbar-buttons>
|
||||
<ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate">
|
||||
<ion-icon name="fas-info-circle" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</core-navbar-buttons>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
|
|
@ -10,6 +10,10 @@ ion-toolbar .in-toolbar.button-clear {
|
|||
--color: var(--ion-color-primary-contrast);
|
||||
}
|
||||
|
||||
ion-toolbar .core-navbar-button-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// Ionic icon.
|
||||
ion-icon {
|
||||
&.icon-slash::after,
|
||||
|
|
Loading…
Reference in New Issue