2020-11-20 12:59:20 +00:00
|
|
|
// (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.
|
|
|
|
|
2021-09-07 06:52:08 +00:00
|
|
|
import {
|
|
|
|
Component,
|
|
|
|
Input,
|
|
|
|
OnInit,
|
|
|
|
OnDestroy,
|
|
|
|
ElementRef,
|
|
|
|
ViewContainerRef,
|
|
|
|
ViewChild,
|
|
|
|
} from '@angular/core';
|
2020-11-20 12:59:20 +00:00
|
|
|
import { CoreLogger } from '@singletons/logger';
|
|
|
|
import { CoreDomUtils } from '@services/utils/dom';
|
|
|
|
import { CoreContextMenuComponent } from '../context-menu/context-menu';
|
2023-01-25 09:34:13 +00:00
|
|
|
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
2022-03-22 09:47:14 +00:00
|
|
|
import { CoreDom } from '@singletons/dom';
|
2020-11-20 12:59:20 +00:00
|
|
|
|
|
|
|
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()">
|
2021-04-27 11:14:31 +00:00
|
|
|
* <ion-icon name="funnel" slot="icon-only" aria-hidden="true"></ion-icon>
|
2020-11-20 12:59:20 +00:00
|
|
|
* </ion-button>
|
|
|
|
* </core-navbar-buttons>
|
|
|
|
*/
|
|
|
|
@Component({
|
|
|
|
selector: 'core-navbar-buttons',
|
2024-01-23 15:13:09 +00:00
|
|
|
template: '<ng-content/><template #contextMenuContainer>-</template>',
|
2020-11-20 12:59:20 +00:00
|
|
|
styleUrls: ['navbar-buttons.scss'],
|
|
|
|
})
|
|
|
|
export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
|
|
|
|
2021-10-21 13:06:45 +00:00
|
|
|
@ViewChild('contextMenuContainer', { read: ViewContainerRef }) container!: ViewContainerRef;
|
2021-09-07 06:52:08 +00:00
|
|
|
|
2020-11-20 12:59:20 +00:00
|
|
|
// If the hidden input is true, hide all buttons.
|
|
|
|
// eslint-disable-next-line @angular-eslint/no-input-rename
|
|
|
|
@Input('hidden') set hidden(value: boolean) {
|
2023-11-29 10:13:02 +00:00
|
|
|
if (typeof value === 'string' && value === '') {
|
2020-11-20 12:59:20 +00:00
|
|
|
value = true;
|
|
|
|
}
|
|
|
|
this.allButtonsHidden = value;
|
|
|
|
this.showHideAllElements();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected element: HTMLElement;
|
|
|
|
protected allButtonsHidden = false;
|
|
|
|
protected forceHidden = false;
|
|
|
|
protected logger: CoreLogger;
|
|
|
|
protected movedChildren?: Node[];
|
|
|
|
protected mergedContextMenu?: CoreContextMenuComponent;
|
2021-09-07 06:52:08 +00:00
|
|
|
protected createdMainContextMenuElement?: HTMLElement;
|
2020-11-20 12:59:20 +00:00
|
|
|
|
2023-11-15 16:02:43 +00:00
|
|
|
constructor(element: ElementRef) {
|
2020-11-20 12:59:20 +00:00
|
|
|
this.element = element.nativeElement;
|
|
|
|
this.logger = CoreLogger.getInstance('CoreNavBarButtonsComponent');
|
2021-06-15 14:16:20 +00:00
|
|
|
|
2023-01-25 09:34:13 +00:00
|
|
|
CoreDirectivesRegistry.register(this.element, this);
|
2020-11-20 12:59:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-03-31 11:42:42 +00:00
|
|
|
* @inheritdoc
|
2020-11-20 12:59:20 +00:00
|
|
|
*/
|
|
|
|
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 + '"]';
|
|
|
|
}
|
|
|
|
|
2022-03-19 15:06:40 +00:00
|
|
|
const buttonsContainer = header.querySelector<HTMLIonButtonsElement>(selector);
|
2020-11-20 12:59:20 +00:00
|
|
|
if (buttonsContainer) {
|
|
|
|
this.mergeContextMenus(buttonsContainer);
|
|
|
|
|
|
|
|
const prepend = this.element.hasAttribute('prepend');
|
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
this.movedChildren = CoreDomUtils.moveChildren(this.element, buttonsContainer, prepend);
|
2020-11-20 12:59:20 +00:00
|
|
|
this.showHideAllElements();
|
|
|
|
|
2021-09-07 06:52:08 +00:00
|
|
|
// Make sure that context-menu is always at the end of buttons if any.
|
|
|
|
const contextMenu = buttonsContainer.querySelector('core-context-menu');
|
2021-10-21 13:06:45 +00:00
|
|
|
const userMenu = buttonsContainer.querySelector('core-user-menu-button');
|
|
|
|
|
|
|
|
if (userMenu) {
|
|
|
|
contextMenu?.parentElement?.insertBefore(contextMenu, userMenu);
|
|
|
|
} else {
|
|
|
|
contextMenu?.parentElement?.appendChild(contextMenu);
|
|
|
|
}
|
2020-11-20 12:59:20 +00:00
|
|
|
} else {
|
|
|
|
this.logger.warn('The header was found, but it didn\'t have the right ion-buttons.', selector);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
// Header not found.
|
2022-03-31 11:42:42 +00:00
|
|
|
this.logger.error(error);
|
2020-11-20 12:59:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2022-03-19 15:06:40 +00:00
|
|
|
protected mergeContextMenus(buttonsContainer: HTMLIonButtonsElement): void {
|
2020-11-20 12:59:20 +00:00
|
|
|
// Check if both button containers have a context menu.
|
|
|
|
const secondaryContextMenu = this.element.querySelector('core-context-menu');
|
|
|
|
if (!secondaryContextMenu) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-09-07 06:52:08 +00:00
|
|
|
const mainContextMenu = buttonsContainer.querySelector('core-context-menu');
|
2023-01-25 09:34:13 +00:00
|
|
|
const secondaryContextMenuInstance = CoreDirectivesRegistry.resolve(secondaryContextMenu, CoreContextMenuComponent);
|
2022-03-01 10:47:57 +00:00
|
|
|
let mainContextMenuInstance: CoreContextMenuComponent | null;
|
2021-09-07 06:52:08 +00:00
|
|
|
if (mainContextMenu) {
|
|
|
|
// Both containers have a context menu. Merge them to prevent having 2 menus at the same time.
|
2023-01-25 09:34:13 +00:00
|
|
|
mainContextMenuInstance = CoreDirectivesRegistry.resolve(mainContextMenu, CoreContextMenuComponent);
|
2021-09-07 06:52:08 +00:00
|
|
|
} else {
|
|
|
|
// There is a context-menu in these buttons, but there is no main context menu in the header.
|
|
|
|
// Create one main context menu dynamically.
|
2023-02-17 10:15:25 +00:00
|
|
|
// @todo: Find a better way to handle header buttons. This isn't working as expected in some cases because the menu
|
|
|
|
// is destroyed when the page is destroyed, so click listeners stop working.
|
2021-09-07 06:52:08 +00:00
|
|
|
mainContextMenuInstance = this.createMainContextMenu();
|
|
|
|
}
|
2020-11-20 12:59:20 +00:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-07 06:52:08 +00:00
|
|
|
/**
|
|
|
|
* Create a new and empty context menu to be used as a "parent".
|
|
|
|
*
|
2022-12-01 11:31:00 +00:00
|
|
|
* @returns Created component.
|
2021-09-07 06:52:08 +00:00
|
|
|
*/
|
|
|
|
protected createMainContextMenu(): CoreContextMenuComponent {
|
2023-11-15 16:02:43 +00:00
|
|
|
const componentRef = this.container.createComponent(CoreContextMenuComponent);
|
2021-09-07 06:52:08 +00:00
|
|
|
|
|
|
|
this.createdMainContextMenuElement = componentRef.location.nativeElement;
|
|
|
|
|
|
|
|
return componentRef.instance;
|
|
|
|
}
|
|
|
|
|
2020-11-20 12:59:20 +00:00
|
|
|
/**
|
|
|
|
* Search the ion-header where the buttons should be added.
|
|
|
|
*
|
2022-12-01 11:31:00 +00:00
|
|
|
* @returns Promise resolved with the header element.
|
2020-11-20 12:59:20 +00:00
|
|
|
*/
|
2022-03-19 15:06:40 +00:00
|
|
|
protected async searchHeader(): Promise<HTMLIonHeaderElement> {
|
2022-03-22 09:47:14 +00:00
|
|
|
await CoreDom.waitToBeInDOM(this.element);
|
2022-03-19 15:06:40 +00:00
|
|
|
let parentPage: HTMLElement | null = this.element;
|
2022-03-31 11:42:42 +00:00
|
|
|
|
2022-03-19 15:06:40 +00:00
|
|
|
while (parentPage && parentPage.parentElement) {
|
2022-03-31 11:42:42 +00:00
|
|
|
const content = parentPage.closest<HTMLIonContentElement>('ion-content');
|
|
|
|
if (content) {
|
|
|
|
// Sometimes ion-page class is not yet added by the ViewController, wait for content to render.
|
|
|
|
await content.componentOnReady();
|
|
|
|
}
|
|
|
|
|
2022-05-11 14:25:26 +00:00
|
|
|
parentPage = parentPage.parentElement.closest('.ion-page, .ion-page-hidden, .ion-page-invisible');
|
2022-03-31 11:42:42 +00:00
|
|
|
|
2022-03-19 15:06:40 +00:00
|
|
|
// Check if the page has a header. If it doesn't, search the next parent page.
|
2022-05-11 14:25:26 +00:00
|
|
|
let header = parentPage?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
|
|
|
|
|
|
|
|
if (header && getComputedStyle(header).display !== 'none') {
|
|
|
|
return header;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find using content if any.
|
|
|
|
header = content?.parentElement?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
|
2022-03-31 11:42:42 +00:00
|
|
|
|
2022-03-19 15:06:40 +00:00
|
|
|
if (header && getComputedStyle(header).display !== 'none') {
|
|
|
|
return header;
|
2020-11-20 12:59:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-19 15:06:40 +00:00
|
|
|
// Header not found, reject.
|
2020-11-20 12:59:20 +00:00
|
|
|
throw Error('Header not found.');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Show or hide all the elements.
|
|
|
|
*/
|
|
|
|
protected showHideAllElements(): void {
|
|
|
|
// Show or hide all moved children.
|
2021-09-07 06:52:08 +00:00
|
|
|
this.movedChildren?.forEach((child: Node) => {
|
|
|
|
this.showHideElement(child);
|
|
|
|
});
|
2020-11-20 12:59:20 +00:00
|
|
|
|
|
|
|
// 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 {
|
2021-09-07 06:52:08 +00:00
|
|
|
// Check if it's an HTML Element and it's not a created context menu. Never hide created context menus.
|
|
|
|
if (element instanceof Element && element !== this.createdMainContextMenuElement) {
|
2020-11-20 12:59:20 +00:00
|
|
|
element.classList.toggle(BUTTON_HIDDEN_CLASS, !!this.forceHidden || !!this.allButtonsHidden);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-03-19 15:06:40 +00:00
|
|
|
* @inheritdoc
|
2020-11-20 12:59:20 +00:00
|
|
|
*/
|
|
|
|
ngOnDestroy(): void {
|
|
|
|
// 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.
|
2021-09-07 06:52:08 +00:00
|
|
|
this.movedChildren?.forEach((child) => {
|
|
|
|
if (child.parentElement && child !== this.createdMainContextMenuElement) {
|
|
|
|
child.parentElement.removeChild(child);
|
|
|
|
}
|
|
|
|
});
|
2020-11-20 12:59:20 +00:00
|
|
|
|
2021-09-07 06:52:08 +00:00
|
|
|
this.mergedContextMenu?.removeMergedItems();
|
2020-11-20 12:59:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|