Merge pull request #3235 from crazyserver/MOBILE-3833

Mobile 3833
main
Noel De Martin 2022-04-06 16:17:27 +02:00 committed by GitHub
commit 080b33f240
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 158 additions and 36 deletions

View File

@ -13,7 +13,17 @@
// limitations under the License. // limitations under the License.
import { BackButtonEvent } from '@ionic/core'; import { BackButtonEvent } from '@ionic/core';
import { AfterViewInit, Component, ElementRef, EventEmitter, HostBinding, Input, Output, ViewChild } from '@angular/core'; import {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
HostBinding,
Input,
OnDestroy,
Output,
ViewChild,
} from '@angular/core';
import { CorePromisedValue } from '@classes/promised-value'; import { CorePromisedValue } from '@classes/promised-value';
import { CoreUserToursFocusLayout } from '@features/usertours/classes/focus-layout'; import { CoreUserToursFocusLayout } from '@features/usertours/classes/focus-layout';
import { CoreUserToursPopoverLayout } from '@features/usertours/classes/popover-layout'; import { CoreUserToursPopoverLayout } from '@features/usertours/classes/popover-layout';
@ -22,6 +32,7 @@ import { CoreDomUtils } from '@services/utils/dom';
import { AngularFrameworkDelegate } from '@singletons'; import { AngularFrameworkDelegate } from '@singletons';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDom } from '@singletons/dom'; import { CoreDom } from '@singletons/dom';
import { CoreEventObserver } from '@singletons/events';
const ANIMATION_DURATION = 200; const ANIMATION_DURATION = 200;
const USER_TOURS_BACK_BUTTON_PRIORITY = 100; const USER_TOURS_BACK_BUTTON_PRIORITY = 100;
@ -36,7 +47,7 @@ const USER_TOURS_BACK_BUTTON_PRIORITY = 100;
templateUrl: 'core-user-tours-user-tour.html', templateUrl: 'core-user-tours-user-tour.html',
styleUrls: ['user-tour.scss'], styleUrls: ['user-tour.scss'],
}) })
export class CoreUserToursUserTourComponent implements AfterViewInit { export class CoreUserToursUserTourComponent implements AfterViewInit, OnDestroy {
@Input() container!: HTMLElement; @Input() container!: HTMLElement;
@Input() id!: string; @Input() id!: string;
@ -59,6 +70,9 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
private wrapperTransform = ''; private wrapperTransform = '';
private wrapperElement = new CorePromisedValue<HTMLElement>(); private wrapperElement = new CorePromisedValue<HTMLElement>();
private backButtonListener?: (event: BackButtonEvent) => void; private backButtonListener?: (event: BackButtonEvent) => void;
protected resizeListener?: CoreEventObserver;
protected scrollListener?: EventListener;
protected content?: HTMLIonContentElement | null;
constructor({ nativeElement: element }: ElementRef<HTMLElement>) { constructor({ nativeElement: element }: ElementRef<HTMLElement>) {
this.element = element; this.element = element;
@ -100,18 +114,7 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
this.calculateStyles(); this.calculateStyles();
// Show tour. this.activate();
this.active = true;
document.addEventListener(
'ionBackButton',
this.backButtonListener = ({ detail }) => detail.register(
USER_TOURS_BACK_BUTTON_PRIORITY,
() => {
// Silence back button.
},
),
);
await this.playEnterAnimation(); await this.playEnterAnimation();
} }
@ -125,8 +128,7 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
await this.playLeaveAnimation(); await this.playLeaveAnimation();
await AngularFrameworkDelegate.removeViewFromDom(wrapper, this.tour); await AngularFrameworkDelegate.removeViewFromDom(wrapper, this.tour);
this.active = false; this.deactivate();
this.backButtonListener && document.removeEventListener('ionBackButton', this.backButtonListener);
} }
/** /**
@ -139,11 +141,11 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
if (this.active) { if (this.active) {
await this.playLeaveAnimation(); await this.playLeaveAnimation();
await AngularFrameworkDelegate.removeViewFromDom(this.container, this.element);
this.backButtonListener && document.removeEventListener('ionBackButton', this.backButtonListener);
} }
await AngularFrameworkDelegate.removeViewFromDom(this.container, this.element);
this.deactivate();
acknowledge && await CoreUserTours.acknowledge(this.id); acknowledge && await CoreUserTours.acknowledge(this.id);
this.afterDismiss.emit(); this.afterDismiss.emit();
@ -205,4 +207,76 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
await Promise.all(animations.map(animation => animation?.finished)); await Promise.all(animations.map(animation => animation?.finished));
} }
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.deactivate();
}
/**
* Activate tour.
*/
protected activate(): void {
if (this.active) {
return;
}
this.active = true;
if (!this.backButtonListener) {
document.addEventListener(
'ionBackButton',
this.backButtonListener = ({ detail }) => detail.register(
USER_TOURS_BACK_BUTTON_PRIORITY,
() => {
// Silence back button.
},
),
);
}
if (!this.focus) {
return;
}
if (!this.resizeListener) {
this.resizeListener = CoreDom.onWindowResize(() => {
this.calculateStyles();
});
}
if (!this.content) {
this.content = CoreDom.closest(this.focus, 'ion-content');
}
if (!this.scrollListener && this.content) {
this.content.scrollEvents = true;
this.content.addEventListener('ionScrollEnd', this.scrollListener = (): void => {
this.calculateStyles();
});
}
}
/**
* Deactivate tour.
*/
protected deactivate(): void {
if (!this.active) {
return;
}
this.active = false;
this.resizeListener?.off();
this.backButtonListener && document.removeEventListener('ionBackButton', this.backButtonListener);
this.backButtonListener = undefined;
this.resizeListener = undefined;
if (this.content && this.scrollListener) {
this.content.removeEventListener('ionScrollEnd', this.scrollListener);
}
}
} }

View File

@ -131,7 +131,7 @@ export class CoreUserToursService {
* @param acknowledge Whether to acknowledge that the user has seen this User Tour or not. * @param acknowledge Whether to acknowledge that the user has seen this User Tour or not.
*/ */
async dismiss(acknowledge: boolean = true): Promise<void> { async dismiss(acknowledge: boolean = true): Promise<void> {
await this.tours.find(({ visible }) => visible)?.component.dismiss(acknowledge); await this.getForegroundTour()?.dismiss(acknowledge);
} }
/** /**
@ -195,7 +195,7 @@ export class CoreUserToursService {
protected activateTour(tour: CoreUserToursUserTourComponent): void { protected activateTour(tour: CoreUserToursUserTourComponent): void {
// Handle show/dismiss lifecycle. // Handle show/dismiss lifecycle.
CoreSubscriptions.once(tour.beforeDismiss, () => { CoreSubscriptions.once(tour.beforeDismiss, () => {
const index = this.tours.findIndex(({ component }) => component === tour); const index = this.getTourIndex(tour);
if (index === -1) { if (index === -1) {
return; return;
@ -203,14 +203,17 @@ export class CoreUserToursService {
this.tours.splice(index, 1); this.tours.splice(index, 1);
const foregroundTour = this.tours.find(({ visible }) => visible); this.getForegroundTour()?.show();
foregroundTour?.component.show();
}); });
// Add to existing tours and show it if it's on top. // Add to existing tours and show it if it's on top.
const index = this.tours.findIndex(({ component }) => component === tour); const index = this.getTourIndex(tour);
const foregroundTour = this.tours.find(({ visible }) => visible); const previousForegroundTour = this.getForegroundTour();
if (previousForegroundTour?.id === tour.id) {
// Already activated.
return;
}
if (index !== -1) { if (index !== -1) {
this.tours[index].visible = true; this.tours[index].visible = true;
@ -221,35 +224,53 @@ export class CoreUserToursService {
}); });
} }
if (this.tours.find(({ visible }) => visible)?.component !== tour) { if (this.getForegroundTour()?.id !== tour.id) {
// Another tour is in use.
return; return;
} }
foregroundTour?.component.hide();
tour.show(); tour.show();
} }
/**
* Returns the first visible tour in the stack.
*
* @return foreground tour if found or undefined.
*/
protected getForegroundTour(): CoreUserToursUserTourComponent | undefined {
return this.tours.find(({ visible }) => visible)?.component;
}
/**
* Returns the tour index in the stack.
*
* @return Tour index if found or -1 otherwise.
*/
protected getTourIndex(tour: CoreUserToursUserTourComponent): number {
return this.tours.findIndex(({ component }) => component === tour);
}
/** /**
* Hide User Tour if visible. * Hide User Tour if visible.
* *
* @param tour User tour. * @param tour User tour.
*/ */
protected deactivateTour(tour: CoreUserToursUserTourComponent): void { protected deactivateTour(tour: CoreUserToursUserTourComponent): void {
const index = this.tours.findIndex(({ component }) => component === tour); const index = this.getTourIndex(tour);
const foregroundTourIndex = this.tours.findIndex(({ visible }) => visible);
if (index === -1) { if (index === -1) {
return; return;
} }
const foregroundTour = this.getForegroundTour();
this.tours[index].visible = false; this.tours[index].visible = false;
if (index === foregroundTourIndex) { if (foregroundTour?.id !== tour.id) {
tour.hide(); // Another tour is in use.
return;
this.tours.find(({ visible }) => visible)?.component.show();
} }
tour.hide();
} }
/** /**

View File

@ -27,6 +27,33 @@ export class CoreDom {
// Nothing to do. // Nothing to do.
} }
/**
* Perform a dom closest function piercing the shadow DOM.
*
* @param node DOM Element.
* @param selector Selector to search.
* @return Closest ancestor or null if not found.
*/
static closest<T = HTMLElement>(node: HTMLElement | Node | null, selector: string): T | null {
if (!node) {
return null;
}
if (node instanceof ShadowRoot) {
return CoreDom.closest(node.host, selector);
}
if (node instanceof HTMLElement) {
if (node.matches(selector)) {
return node as unknown as T;
} else {
return CoreDom.closest<T>(node.parentNode, selector);
}
}
return CoreDom.closest<T>(node.parentNode, selector);
}
/** /**
* Retrieve the position of a element relative to another element. * Retrieve the position of a element relative to another element.
* *