MOBILE-2335 contextmenu: Merge menus in navbuttons component
This commit is contained in:
		
							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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user