commit
080b33f240
|
@ -13,7 +13,17 @@
|
|||
// limitations under the License.
|
||||
|
||||
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 { CoreUserToursFocusLayout } from '@features/usertours/classes/focus-layout';
|
||||
import { CoreUserToursPopoverLayout } from '@features/usertours/classes/popover-layout';
|
||||
|
@ -22,6 +32,7 @@ import { CoreDomUtils } from '@services/utils/dom';
|
|||
import { AngularFrameworkDelegate } from '@singletons';
|
||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||
import { CoreDom } from '@singletons/dom';
|
||||
import { CoreEventObserver } from '@singletons/events';
|
||||
|
||||
const ANIMATION_DURATION = 200;
|
||||
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',
|
||||
styleUrls: ['user-tour.scss'],
|
||||
})
|
||||
export class CoreUserToursUserTourComponent implements AfterViewInit {
|
||||
export class CoreUserToursUserTourComponent implements AfterViewInit, OnDestroy {
|
||||
|
||||
@Input() container!: HTMLElement;
|
||||
@Input() id!: string;
|
||||
|
@ -59,6 +70,9 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
|
|||
private wrapperTransform = '';
|
||||
private wrapperElement = new CorePromisedValue<HTMLElement>();
|
||||
private backButtonListener?: (event: BackButtonEvent) => void;
|
||||
protected resizeListener?: CoreEventObserver;
|
||||
protected scrollListener?: EventListener;
|
||||
protected content?: HTMLIonContentElement | null;
|
||||
|
||||
constructor({ nativeElement: element }: ElementRef<HTMLElement>) {
|
||||
this.element = element;
|
||||
|
@ -100,18 +114,7 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
|
|||
|
||||
this.calculateStyles();
|
||||
|
||||
// Show tour.
|
||||
this.active = true;
|
||||
|
||||
document.addEventListener(
|
||||
'ionBackButton',
|
||||
this.backButtonListener = ({ detail }) => detail.register(
|
||||
USER_TOURS_BACK_BUTTON_PRIORITY,
|
||||
() => {
|
||||
// Silence back button.
|
||||
},
|
||||
),
|
||||
);
|
||||
this.activate();
|
||||
|
||||
await this.playEnterAnimation();
|
||||
}
|
||||
|
@ -125,8 +128,7 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
|
|||
await this.playLeaveAnimation();
|
||||
await AngularFrameworkDelegate.removeViewFromDom(wrapper, this.tour);
|
||||
|
||||
this.active = false;
|
||||
this.backButtonListener && document.removeEventListener('ionBackButton', this.backButtonListener);
|
||||
this.deactivate();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -139,11 +141,11 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
|
|||
|
||||
if (this.active) {
|
||||
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);
|
||||
|
||||
this.afterDismiss.emit();
|
||||
|
@ -205,4 +207,76 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -131,7 +131,7 @@ export class CoreUserToursService {
|
|||
* @param acknowledge Whether to acknowledge that the user has seen this User Tour or not.
|
||||
*/
|
||||
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 {
|
||||
// Handle show/dismiss lifecycle.
|
||||
CoreSubscriptions.once(tour.beforeDismiss, () => {
|
||||
const index = this.tours.findIndex(({ component }) => component === tour);
|
||||
const index = this.getTourIndex(tour);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
|
@ -203,14 +203,17 @@ export class CoreUserToursService {
|
|||
|
||||
this.tours.splice(index, 1);
|
||||
|
||||
const foregroundTour = this.tours.find(({ visible }) => visible);
|
||||
|
||||
foregroundTour?.component.show();
|
||||
this.getForegroundTour()?.show();
|
||||
});
|
||||
|
||||
// Add to existing tours and show it if it's on top.
|
||||
const index = this.tours.findIndex(({ component }) => component === tour);
|
||||
const foregroundTour = this.tours.find(({ visible }) => visible);
|
||||
const index = this.getTourIndex(tour);
|
||||
const previousForegroundTour = this.getForegroundTour();
|
||||
|
||||
if (previousForegroundTour?.id === tour.id) {
|
||||
// Already activated.
|
||||
return;
|
||||
}
|
||||
|
||||
if (index !== -1) {
|
||||
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;
|
||||
}
|
||||
|
||||
foregroundTour?.component.hide();
|
||||
|
||||
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.
|
||||
*
|
||||
* @param tour User tour.
|
||||
*/
|
||||
protected deactivateTour(tour: CoreUserToursUserTourComponent): void {
|
||||
const index = this.tours.findIndex(({ component }) => component === tour);
|
||||
const foregroundTourIndex = this.tours.findIndex(({ visible }) => visible);
|
||||
|
||||
const index = this.getTourIndex(tour);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const foregroundTour = this.getForegroundTour();
|
||||
|
||||
this.tours[index].visible = false;
|
||||
|
||||
if (index === foregroundTourIndex) {
|
||||
tour.hide();
|
||||
|
||||
this.tours.find(({ visible }) => visible)?.component.show();
|
||||
if (foregroundTour?.id !== tour.id) {
|
||||
// Another tour is in use.
|
||||
return;
|
||||
}
|
||||
|
||||
tour.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -27,6 +27,33 @@ export class CoreDom {
|
|||
// 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.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue