MOBILE-3594 core: Implement context menu component

main
Pau Ferrer Ocaña 2020-11-20 13:59:20 +01:00
parent add521a0e7
commit 1ffcf4c877
14 changed files with 765 additions and 11 deletions

View File

@ -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 {}

View 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();
}
}
}

View File

@ -0,0 +1,5 @@
:host {
ion-list {
padding: 0;
}
}

View 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;
}
}

View 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();
}
}

View File

@ -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>

View 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>

View File

@ -0,0 +1,3 @@
:host {
display: none !important;
}

View 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();
}
}
}

View File

@ -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>

View File

@ -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']);
}
} }

View File

@ -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>

View File

@ -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>

View File

@ -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,