2018-01-25 14:49:15 +01:00
|
|
|
// (C) Copyright 2015 Martin Dougiamas
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2018-02-22 09:27:49 +01:00
|
|
|
import { Component, Input, OnInit, OnDestroy, ContentChildren, ElementRef, QueryList } from '@angular/core';
|
2018-01-25 14:49:15 +01:00
|
|
|
import { Button } from 'ionic-angular';
|
2018-03-01 16:55:49 +01:00
|
|
|
import { CoreLoggerProvider } from '@providers/logger';
|
|
|
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
2018-02-22 11:58:08 +01:00
|
|
|
import { CoreContextMenuComponent } from '../context-menu/context-menu';
|
2018-01-25 14:49:15 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Component to add buttons to the app's header without having to place them inside the header itself. This is meant for
|
|
|
|
* pages that are loaded inside a sub ion-nav, so they don't have a header.
|
|
|
|
*
|
|
|
|
* If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that
|
|
|
|
* position. If no start/end is specified, then the buttons will be added to the first <ion-buttons> found in the header.
|
|
|
|
*
|
|
|
|
* You can use the [hidden] input to hide all the inner buttons if a certain condition is met.
|
|
|
|
*
|
2018-02-22 09:27:49 +01:00
|
|
|
* IMPORTANT: Do not use *ngIf in the buttons inside this component, it can cause problems. Please use [hidden] instead.
|
|
|
|
*
|
2018-01-25 14:49:15 +01:00
|
|
|
* Example usage:
|
|
|
|
*
|
|
|
|
* <core-navbar-buttons end>
|
2018-02-22 09:27:49 +01:00
|
|
|
* <button ion-button icon-only [hidden]="!buttonShown" [attr.aria-label]="Do something" (click)="action()">
|
2018-01-25 14:49:15 +01:00
|
|
|
* <ion-icon name="funnel"></ion-icon>
|
|
|
|
* </button>
|
|
|
|
* </core-navbar-buttons>
|
|
|
|
*/
|
|
|
|
@Component({
|
|
|
|
selector: 'core-navbar-buttons',
|
|
|
|
template: '<ng-content></ng-content>'
|
|
|
|
})
|
2018-02-22 09:27:49 +01:00
|
|
|
export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
2018-01-25 14:49:15 +01:00
|
|
|
|
|
|
|
protected BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden';
|
|
|
|
|
|
|
|
// If the hidden input is true, hide all buttons.
|
|
|
|
@Input('hidden') set hidden(value: boolean) {
|
|
|
|
this._hidden = value;
|
2018-02-22 09:27:49 +01:00
|
|
|
this.showHideAllElements();
|
2018-01-25 14:49:15 +01:00
|
|
|
}
|
|
|
|
|
2018-02-22 09:27:49 +01:00
|
|
|
// Get all the ion-buttons inside this directive and apply the role bar-button.
|
2018-01-25 14:49:15 +01:00
|
|
|
@ContentChildren(Button) set buttons(buttons: QueryList<Button>) {
|
|
|
|
buttons.forEach((button: Button) => {
|
|
|
|
button.setRole('bar-button');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
protected element: HTMLElement;
|
|
|
|
protected _hidden: boolean;
|
2018-02-22 11:58:08 +01:00
|
|
|
protected forceHidden = false;
|
2018-02-05 12:31:48 +01:00
|
|
|
protected logger: any;
|
2018-02-22 09:27:49 +01:00
|
|
|
protected movedChildren: Node[];
|
2018-02-22 11:58:08 +01:00
|
|
|
protected instanceId: string;
|
|
|
|
protected mergedContextMenu: CoreContextMenuComponent;
|
2018-01-25 14:49:15 +01:00
|
|
|
|
2018-02-05 12:31:48 +01:00
|
|
|
constructor(element: ElementRef, logger: CoreLoggerProvider, private domUtils: CoreDomUtilsProvider) {
|
2018-01-25 14:49:15 +01:00
|
|
|
this.element = element.nativeElement;
|
2018-02-05 12:31:48 +01:00
|
|
|
this.logger = logger.getInstance('CoreNavBarButtonsComponent');
|
2018-02-22 11:58:08 +01:00
|
|
|
this.instanceId = this.domUtils.storeInstanceByElement(this.element, this);
|
2018-01-25 14:49:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component being initialized.
|
|
|
|
*/
|
|
|
|
ngOnInit(): void {
|
2018-02-05 12:31:48 +01:00
|
|
|
this.searchHeader().then((header) => {
|
|
|
|
if (header) {
|
|
|
|
// Search the right buttons container (start, end or any).
|
|
|
|
let selector = 'ion-buttons',
|
|
|
|
buttonsContainer: HTMLElement;
|
|
|
|
|
|
|
|
if (this.element.hasAttribute('start')) {
|
|
|
|
selector += '[start]';
|
|
|
|
} else if (this.element.hasAttribute('end')) {
|
|
|
|
selector += '[end]';
|
|
|
|
}
|
2018-01-25 14:49:15 +01:00
|
|
|
|
2018-02-05 12:31:48 +01:00
|
|
|
buttonsContainer = <HTMLElement> header.querySelector(selector);
|
|
|
|
if (buttonsContainer) {
|
|
|
|
this.mergeContextMenus(buttonsContainer);
|
2018-01-25 14:49:15 +01:00
|
|
|
|
2018-02-22 09:27:49 +01:00
|
|
|
this.movedChildren = this.domUtils.moveChildren(this.element, buttonsContainer);
|
|
|
|
this.showHideAllElements();
|
|
|
|
|
2018-02-05 12:31:48 +01:00
|
|
|
} else {
|
|
|
|
this.logger.warn('The header was found, but it didn\'t have the right ion-buttons.', selector);
|
|
|
|
}
|
2018-01-25 14:49:15 +01:00
|
|
|
}
|
2018-02-05 12:31:48 +01:00
|
|
|
}).catch(() => {
|
|
|
|
// Header not found.
|
|
|
|
this.logger.warn('Header not found.');
|
|
|
|
});
|
|
|
|
}
|
2018-01-25 14:49:15 +01:00
|
|
|
|
2018-02-22 11:58:08 +01:00
|
|
|
/**
|
|
|
|
* Force or unforce hiding all buttons. If this is true, it will override the "hidden" input.
|
|
|
|
*
|
|
|
|
* @param {boolean} value The value to set.
|
|
|
|
*/
|
|
|
|
forceHide(value: boolean): void {
|
|
|
|
this.forceHidden = value;
|
|
|
|
|
|
|
|
this.showHideAllElements();
|
|
|
|
}
|
|
|
|
|
2018-02-05 12:31:48 +01:00
|
|
|
/**
|
|
|
|
* If both button containers have a context menu, merge them into a single one.
|
|
|
|
*
|
|
|
|
* @param {HTMLElement} buttonsContainer The container where the buttons will be moved.
|
|
|
|
*/
|
|
|
|
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.
|
2018-03-20 13:03:35 +01:00
|
|
|
const mainContextMenuInstance: CoreContextMenuComponent = this.domUtils.getInstanceByElement(mainContextMenu),
|
|
|
|
secondaryContextMenuInstance: CoreContextMenuComponent = this.domUtils.getInstanceByElement(secondaryContextMenu);
|
2018-02-05 12:31:48 +01:00
|
|
|
|
2018-03-20 13:03:35 +01:00
|
|
|
// Check that both context menus belong to the same core-tab. We shouldn't merge menus from different tabs.
|
|
|
|
if (mainContextMenuInstance && secondaryContextMenuInstance &&
|
|
|
|
mainContextMenuInstance.coreTab === secondaryContextMenuInstance.coreTab) {
|
2018-02-22 11:58:08 +01:00
|
|
|
this.mergedContextMenu = secondaryContextMenuInstance;
|
|
|
|
|
|
|
|
this.mergedContextMenu.mergeContextMenus(mainContextMenuInstance);
|
2018-02-05 12:31:48 +01:00
|
|
|
|
|
|
|
// Remove the empty context menu from the DOM.
|
|
|
|
secondaryContextMenu.parentElement.removeChild(secondaryContextMenu);
|
2018-01-25 14:49:15 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search the ion-header where the buttons should be added.
|
|
|
|
*
|
2018-02-05 12:31:48 +01:00
|
|
|
* @param {number} [retries] Number of retries so far.
|
|
|
|
* @return {Promise<HTMLElement>} Promise resolved with the header element.
|
2018-01-25 14:49:15 +01:00
|
|
|
*/
|
2018-02-05 12:31:48 +01:00
|
|
|
protected searchHeader(retries: number = 0): Promise<HTMLElement> {
|
2018-01-25 14:49:15 +01:00
|
|
|
let parentPage: HTMLElement = this.element;
|
|
|
|
|
|
|
|
while (parentPage) {
|
|
|
|
if (!parentPage.parentElement) {
|
|
|
|
// No parent, stop.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the next parent page.
|
|
|
|
parentPage = <HTMLElement> this.domUtils.closest(parentPage.parentElement, '.ion-page');
|
|
|
|
if (parentPage) {
|
|
|
|
// Check if the page has a header. If it doesn't, search the next parent page.
|
|
|
|
const header = this.searchHeaderInPage(parentPage);
|
2018-02-14 17:19:09 +01:00
|
|
|
if (header && getComputedStyle(header, null).display != 'none') {
|
2018-02-05 12:31:48 +01:00
|
|
|
return Promise.resolve(header);
|
2018-01-25 14:49:15 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-02-05 12:31:48 +01:00
|
|
|
|
|
|
|
// 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(() => {
|
|
|
|
this.searchHeader(retries + 1).then(resolve, reject);
|
|
|
|
}, 200);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// We've waited enough time, reject.
|
|
|
|
return Promise.reject(null);
|
2018-01-25 14:49:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search ion-header inside a page. The header should be a direct child.
|
|
|
|
*
|
|
|
|
* @param {HTMLElement} page Page to search in.
|
|
|
|
* @return {HTMLElement} Header element. Undefined if not found.
|
|
|
|
*/
|
|
|
|
protected searchHeaderInPage(page: HTMLElement): HTMLElement {
|
|
|
|
for (let i = 0; i < page.children.length; i++) {
|
|
|
|
const child = page.children[i];
|
|
|
|
if (child.tagName == 'ION-HEADER') {
|
|
|
|
return <HTMLElement> child;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-02-22 09:27:49 +01:00
|
|
|
* Show or hide all the elements.
|
|
|
|
*/
|
|
|
|
protected showHideAllElements(): void {
|
2018-02-22 11:58:08 +01:00
|
|
|
// Show or hide all moved children.
|
2018-02-22 09:27:49 +01:00
|
|
|
if (this.movedChildren) {
|
|
|
|
this.movedChildren.forEach((child: Node) => {
|
|
|
|
this.showHideElement(child);
|
|
|
|
});
|
|
|
|
}
|
2018-02-22 11:58:08 +01:00
|
|
|
|
|
|
|
// Show or hide all the context menu items that were merged to another context menu.
|
|
|
|
if (this.mergedContextMenu) {
|
|
|
|
if (this.forceHidden || this._hidden) {
|
|
|
|
this.mergedContextMenu.removeMergedItems();
|
|
|
|
} else {
|
|
|
|
this.mergedContextMenu.restoreMergedItems();
|
|
|
|
}
|
|
|
|
}
|
2018-02-22 09:27:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Show or hide an element.
|
2018-01-25 14:49:15 +01:00
|
|
|
*
|
2018-02-22 09:27:49 +01:00
|
|
|
* @param {Node} element Element to show or hide.
|
2018-01-25 14:49:15 +01:00
|
|
|
*/
|
2018-02-22 09:27:49 +01:00
|
|
|
protected showHideElement(element: Node): void {
|
|
|
|
// Check if it's an HTML Element
|
|
|
|
if (element instanceof Element) {
|
2018-02-22 11:58:08 +01:00
|
|
|
if (this.forceHidden || this._hidden) {
|
2018-02-22 09:27:49 +01:00
|
|
|
element.classList.add(this.BUTTON_HIDDEN_CLASS);
|
|
|
|
} else {
|
|
|
|
element.classList.remove(this.BUTTON_HIDDEN_CLASS);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component destroyed.
|
|
|
|
*/
|
|
|
|
ngOnDestroy(): void {
|
2018-02-22 11:58:08 +01:00
|
|
|
this.domUtils.removeInstanceById(this.instanceId);
|
|
|
|
|
2018-02-22 09:27:49 +01:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
});
|
2018-01-25 14:49:15 +01:00
|
|
|
}
|
2018-02-22 11:58:08 +01:00
|
|
|
|
|
|
|
if (this.mergedContextMenu) {
|
|
|
|
this.mergedContextMenu.removeMergedItems();
|
|
|
|
}
|
2018-01-25 14:49:15 +01:00
|
|
|
}
|
|
|
|
}
|