commit
080b33f240
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue