MOBILE-2335 contextmenu: Merge menus in navbuttons component
parent
6af7e4195d
commit
577a0a6598
|
@ -12,9 +12,10 @@
|
||||||
// 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 { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core';
|
||||||
import { PopoverController } from 'ionic-angular';
|
import { PopoverController } from 'ionic-angular';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreDomUtilsProvider } from '../../providers/utils/dom';
|
||||||
import { CoreContextMenuItemComponent } from './context-menu-item';
|
import { CoreContextMenuItemComponent } from './context-menu-item';
|
||||||
import { CoreContextMenuPopoverComponent } from './context-menu-popover';
|
import { CoreContextMenuPopoverComponent } from './context-menu-popover';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
@ -26,7 +27,7 @@ import { Subject } from 'rxjs';
|
||||||
selector: 'core-context-menu',
|
selector: 'core-context-menu',
|
||||||
templateUrl: 'context-menu.html'
|
templateUrl: 'context-menu.html'
|
||||||
})
|
})
|
||||||
export class CoreContextMenuComponent implements OnInit {
|
export class CoreContextMenuComponent implements OnInit, OnDestroy {
|
||||||
@Input() icon?: string; // Icon to be shown on the navigation bar. Default: Kebab menu icon.
|
@Input() icon?: string; // Icon to be shown on the navigation bar. Default: Kebab menu icon.
|
||||||
@Input() title?: string; // Aria label and text to be shown on the top of the popover.
|
@Input() title?: string; // Aria label and text to be shown on the top of the popover.
|
||||||
|
|
||||||
|
@ -34,8 +35,11 @@ export class CoreContextMenuComponent implements OnInit {
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
protected items: CoreContextMenuItemComponent[] = [];
|
protected items: CoreContextMenuItemComponent[] = [];
|
||||||
protected itemsChangedStream: Subject<void>; // Stream to update the hideMenu boolean when items change.
|
protected itemsChangedStream: Subject<void>; // Stream to update the hideMenu boolean when items change.
|
||||||
|
protected instanceId: string;
|
||||||
|
protected parentContextMenu: CoreContextMenuComponent;
|
||||||
|
|
||||||
constructor(private translate: TranslateService, private popoverCtrl: PopoverController) {
|
constructor(private translate: TranslateService, private popoverCtrl: PopoverController, private elementRef: ElementRef,
|
||||||
|
private domUtils: CoreDomUtilsProvider) {
|
||||||
// Create the stream and subscribe to it. We ignore successive changes during 250ms.
|
// Create the stream and subscribe to it. We ignore successive changes during 250ms.
|
||||||
this.itemsChangedStream = new Subject<void>();
|
this.itemsChangedStream = new Subject<void>();
|
||||||
this.itemsChangedStream.auditTime(250).subscribe(() => {
|
this.itemsChangedStream.auditTime(250).subscribe(() => {
|
||||||
|
@ -49,6 +53,8 @@ export class CoreContextMenuComponent implements OnInit {
|
||||||
return a.priority <= b.priority ? 1 : -1;
|
return a.priority <= b.priority ? 1 : -1;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.instanceId = this.domUtils.storeInstanceByElement(elementRef.nativeElement, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -65,15 +71,44 @@ export class CoreContextMenuComponent implements OnInit {
|
||||||
* @param {CoreContextMenuItemComponent} item The item to add.
|
* @param {CoreContextMenuItemComponent} item The item to add.
|
||||||
*/
|
*/
|
||||||
addItem(item: CoreContextMenuItemComponent): void {
|
addItem(item: CoreContextMenuItemComponent): void {
|
||||||
this.items.push(item);
|
if (this.parentContextMenu) {
|
||||||
this.itemsChanged();
|
// All items were moved to the "parent" menu. Add the item in there.
|
||||||
|
this.parentContextMenu.addItem(item);
|
||||||
|
} else {
|
||||||
|
this.items.push(item);
|
||||||
|
this.itemsChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function called when the items change.
|
* Function called when the items change.
|
||||||
*/
|
*/
|
||||||
itemsChanged(): void {
|
itemsChanged(): void {
|
||||||
this.itemsChangedStream.next();
|
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 {CoreContextMenuComponent} 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++) {
|
||||||
|
contextMenu.addItem(this.items[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all items from the current menu.
|
||||||
|
this.items = [];
|
||||||
|
this.itemsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,11 +117,16 @@ export class CoreContextMenuComponent implements OnInit {
|
||||||
* @param {CoreContextMenuItemComponent} item The item to remove.
|
* @param {CoreContextMenuItemComponent} item The item to remove.
|
||||||
*/
|
*/
|
||||||
removeItem(item: CoreContextMenuItemComponent): void {
|
removeItem(item: CoreContextMenuItemComponent): void {
|
||||||
const index = this.items.indexOf(item);
|
if (this.parentContextMenu) {
|
||||||
if (index >= 0) {
|
// All items were moved to the "parent" menu. Remove the item from there.
|
||||||
this.items.splice(index, 1);
|
this.parentContextMenu.removeItem(item);
|
||||||
|
} else {
|
||||||
|
const index = this.items.indexOf(item);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.items.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.itemsChanged();
|
||||||
}
|
}
|
||||||
this.itemsChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,4 +140,11 @@ export class CoreContextMenuComponent implements OnInit {
|
||||||
ev: event
|
ev: event
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.domUtils.removeInstanceById(this.instanceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
import { Component, Input, OnInit, ContentChildren, ElementRef, QueryList } from '@angular/core';
|
import { Component, Input, OnInit, ContentChildren, ElementRef, QueryList } from '@angular/core';
|
||||||
import { Button } from 'ionic-angular';
|
import { Button } from 'ionic-angular';
|
||||||
|
import { CoreLoggerProvider } from '../../providers/logger';
|
||||||
import { CoreDomUtilsProvider } from '../../providers/utils/dom';
|
import { CoreDomUtilsProvider } from '../../providers/utils/dom';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,41 +64,80 @@ export class CoreNavBarButtonsComponent implements OnInit {
|
||||||
protected element: HTMLElement;
|
protected element: HTMLElement;
|
||||||
protected _buttons: QueryList<Button>;
|
protected _buttons: QueryList<Button>;
|
||||||
protected _hidden: boolean;
|
protected _hidden: boolean;
|
||||||
|
protected logger: any;
|
||||||
|
|
||||||
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider) {
|
constructor(element: ElementRef, logger: CoreLoggerProvider, private domUtils: CoreDomUtilsProvider) {
|
||||||
this.element = element.nativeElement;
|
this.element = element.nativeElement;
|
||||||
|
this.logger = logger.getInstance('CoreNavBarButtonsComponent');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const header = this.searchHeader();
|
this.searchHeader().then((header) => {
|
||||||
|
if (header) {
|
||||||
|
// Search the right buttons container (start, end or any).
|
||||||
|
let selector = 'ion-buttons',
|
||||||
|
buttonsContainer: HTMLElement;
|
||||||
|
|
||||||
if (header) {
|
if (this.element.hasAttribute('start')) {
|
||||||
// Search the right buttons container (start, end or any).
|
selector += '[start]';
|
||||||
let selector = 'ion-buttons',
|
} else if (this.element.hasAttribute('end')) {
|
||||||
buttonsContainer: HTMLElement;
|
selector += '[end]';
|
||||||
|
}
|
||||||
|
|
||||||
if (this.element.hasAttribute('start')) {
|
buttonsContainer = <HTMLElement> header.querySelector(selector);
|
||||||
selector += '[start]';
|
if (buttonsContainer) {
|
||||||
} else if (this.element.hasAttribute('end')) {
|
this.mergeContextMenus(buttonsContainer);
|
||||||
selector += '[end]';
|
|
||||||
|
this.domUtils.moveChildren(this.element, buttonsContainer);
|
||||||
|
} else {
|
||||||
|
this.logger.warn('The header was found, but it didn\'t have the right ion-buttons.', selector);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// Header not found.
|
||||||
|
this.logger.warn('Header not found.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
buttonsContainer = <HTMLElement> header.querySelector(selector);
|
/**
|
||||||
if (buttonsContainer) {
|
* If both button containers have a context menu, merge them into a single one.
|
||||||
this.domUtils.moveChildren(this.element, buttonsContainer);
|
*
|
||||||
}
|
* @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.
|
||||||
|
const mainContextMenuInstance = this.domUtils.getInstanceByElement(mainContextMenu),
|
||||||
|
secondaryContextMenuInstance = this.domUtils.getInstanceByElement(secondaryContextMenu);
|
||||||
|
|
||||||
|
if (mainContextMenuInstance && secondaryContextMenuInstance) {
|
||||||
|
secondaryContextMenuInstance.mergeContextMenus(mainContextMenuInstance);
|
||||||
|
|
||||||
|
// Remove the empty context menu from the DOM.
|
||||||
|
secondaryContextMenu.parentElement.removeChild(secondaryContextMenu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search the ion-header where the buttons should be added.
|
* Search the ion-header where the buttons should be added.
|
||||||
*
|
*
|
||||||
* @return {HTMLElement} Header element.
|
* @param {number} [retries] Number of retries so far.
|
||||||
|
* @return {Promise<HTMLElement>} Promise resolved with the header element.
|
||||||
*/
|
*/
|
||||||
protected searchHeader(): HTMLElement {
|
protected searchHeader(retries: number = 0): Promise<HTMLElement> {
|
||||||
let parentPage: HTMLElement = this.element;
|
let parentPage: HTMLElement = this.element;
|
||||||
|
|
||||||
while (parentPage) {
|
while (parentPage) {
|
||||||
|
@ -112,10 +152,24 @@ export class CoreNavBarButtonsComponent implements OnInit {
|
||||||
// Check if the page has a header. If it doesn't, search the next parent page.
|
// Check if the page has a header. If it doesn't, search the next parent page.
|
||||||
const header = this.searchHeaderInPage(parentPage);
|
const header = this.searchHeaderInPage(parentPage);
|
||||||
if (header) {
|
if (header) {
|
||||||
return header;
|
return Promise.resolve(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(() => {
|
||||||
|
this.searchHeader(retries + 1).then(resolve, reject);
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've waited enough time, reject.
|
||||||
|
return Promise.reject(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
<ion-buttons end>
|
<ion-buttons end>
|
||||||
<core-context-menu>
|
<core-context-menu>
|
||||||
<core-context-menu-item [priority]="900" [content]="'core.settings.enabledownloadsection' | translate" (action)="toggleDownload()" [iconAction]="downloadEnabledIcon"></core-context-menu-item>
|
<core-context-menu-item [priority]="2000" [content]="'core.settings.enabledownloadsection' | translate" (action)="toggleDownload()" [iconAction]="downloadEnabledIcon"></core-context-menu-item>
|
||||||
<core-context-menu-item [priority]="850" [content]="'core.course.downloadcourse' | translate" (action)="prefetchCourse()" [iconAction]="prefetchCourseData.prefetchCourseIcon" [closeOnClick]="false"></core-context-menu-item>
|
<core-context-menu-item [priority]="1900" [content]="'core.course.downloadcourse' | translate" (action)="prefetchCourse()" [iconAction]="prefetchCourseData.prefetchCourseIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
</core-context-menu>
|
</core-context-menu>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
</ion-navbar>
|
</ion-navbar>
|
||||||
|
|
|
@ -32,9 +32,12 @@ export class CoreDomUtilsProvider {
|
||||||
// List of input types that support keyboard.
|
// List of input types that support keyboard.
|
||||||
protected INPUT_SUPPORT_KEYBOARD = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number', 'password',
|
protected INPUT_SUPPORT_KEYBOARD = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number', 'password',
|
||||||
'search', 'tel', 'text', 'time', 'url', 'week'];
|
'search', 'tel', 'text', 'time', 'url', 'week'];
|
||||||
|
protected INSTANCE_ID_ATTR_NAME = 'core-instance-id';
|
||||||
|
|
||||||
protected element = document.createElement('div'); // Fake element to use in some functions, to prevent creating it each time.
|
protected element = document.createElement('div'); // Fake element to use in some functions, to prevent creating it each time.
|
||||||
protected matchesFn: string; // Name of the "matches" function to use when simulating a closest call.
|
protected matchesFn: string; // Name of the "matches" function to use when simulating a closest call.
|
||||||
|
protected instances: {[id: string]: any} = {}; // Store component/directive instances by id.
|
||||||
|
protected lastInstanceId = 0;
|
||||||
|
|
||||||
constructor(private translate: TranslateService, private loadingCtrl: LoadingController, private toastCtrl: ToastController,
|
constructor(private translate: TranslateService, private loadingCtrl: LoadingController, private toastCtrl: ToastController,
|
||||||
private alertCtrl: AlertController, private textUtils: CoreTextUtilsProvider, private appProvider: CoreAppProvider,
|
private alertCtrl: AlertController, private textUtils: CoreTextUtilsProvider, private appProvider: CoreAppProvider,
|
||||||
|
@ -410,6 +413,20 @@ export class CoreDomUtilsProvider {
|
||||||
return this.textUtils.decodeHTML(this.translate.instant('core.error'));
|
return this.textUtils.decodeHTML(this.translate.instant('core.error'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve component/directive instance.
|
||||||
|
* Please use this function only if you cannot retrieve the instance using parent/child methods: ViewChild (or similar)
|
||||||
|
* or Angular's injection.
|
||||||
|
*
|
||||||
|
* @param {Element} element The root element of the component/directive.
|
||||||
|
* @return {any} The instance, undefined if not found.
|
||||||
|
*/
|
||||||
|
getInstanceByElement(element: Element): any {
|
||||||
|
const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME);
|
||||||
|
|
||||||
|
return this.instances[id];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an element is outside of screen (viewport).
|
* Check if an element is outside of screen (viewport).
|
||||||
*
|
*
|
||||||
|
@ -513,6 +530,25 @@ export class CoreDomUtilsProvider {
|
||||||
return this.element.innerHTML;
|
return this.element.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a component/directive instance using the DOM Element.
|
||||||
|
*
|
||||||
|
* @param {Element} element The root element of the component/directive.
|
||||||
|
*/
|
||||||
|
removeInstanceByElement(element: Element): void {
|
||||||
|
const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME);
|
||||||
|
delete this.instances[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a component/directive instance using the ID.
|
||||||
|
*
|
||||||
|
* @param {string} id The ID to remove.
|
||||||
|
*/
|
||||||
|
removeInstanceById(id: string): void {
|
||||||
|
delete this.instances[id];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for certain classes in an element contents and replace them with the specified new values.
|
* Search for certain classes in an element contents and replace them with the specified new values.
|
||||||
*
|
*
|
||||||
|
@ -883,6 +919,22 @@ export class CoreDomUtilsProvider {
|
||||||
return loader;
|
return loader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a component/directive instance.
|
||||||
|
*
|
||||||
|
* @param {Element} element The root element of the component/directive.
|
||||||
|
* @param {any} instance The instance to store.
|
||||||
|
* @return {string} ID to identify the instance.
|
||||||
|
*/
|
||||||
|
storeInstanceByElement(element: Element, instance: any): string {
|
||||||
|
const id = String(this.lastInstanceId++);
|
||||||
|
|
||||||
|
element.setAttribute(this.INSTANCE_ID_ATTR_NAME, id);
|
||||||
|
this.instances[id] = instance;
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an element supports input via keyboard.
|
* Check if an element supports input via keyboard.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue