forked from CIT/Vmeda.Online
		
	MOBILE-3833 usertours: Watch elements visibility
This commit is contained in:
		
							parent
							
								
									771ee90d6a
								
							
						
					
					
						commit
						35433615f3
					
				@ -32,8 +32,8 @@ import { CoreSwipeNavigationDirective } from './swipe-navigation';
 | 
			
		||||
import { CoreCollapsibleItemDirective } from './collapsible-item';
 | 
			
		||||
import { CoreCollapsibleFooterDirective } from './collapsible-footer';
 | 
			
		||||
import { CoreContentDirective } from './content';
 | 
			
		||||
import { CoreOnAppearDirective } from './on-appear';
 | 
			
		||||
import { CoreUpdateNonReactiveAttributesDirective } from './update-non-reactive-attributes';
 | 
			
		||||
import { CoreUserTourDirective } from './user-tour';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
@ -48,7 +48,6 @@ import { CoreUpdateNonReactiveAttributesDirective } from './update-non-reactive-
 | 
			
		||||
        CoreSupressEventsDirective,
 | 
			
		||||
        CoreUserLinkDirective,
 | 
			
		||||
        CoreAriaButtonClickDirective,
 | 
			
		||||
        CoreOnAppearDirective,
 | 
			
		||||
        CoreOnResizeDirective,
 | 
			
		||||
        CoreDownloadFileDirective,
 | 
			
		||||
        CoreCollapsibleHeaderDirective,
 | 
			
		||||
@ -57,6 +56,7 @@ import { CoreUpdateNonReactiveAttributesDirective } from './update-non-reactive-
 | 
			
		||||
        CoreCollapsibleFooterDirective,
 | 
			
		||||
        CoreContentDirective,
 | 
			
		||||
        CoreUpdateNonReactiveAttributesDirective,
 | 
			
		||||
        CoreUserTourDirective,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        CoreAutoFocusDirective,
 | 
			
		||||
@ -70,7 +70,6 @@ import { CoreUpdateNonReactiveAttributesDirective } from './update-non-reactive-
 | 
			
		||||
        CoreSupressEventsDirective,
 | 
			
		||||
        CoreUserLinkDirective,
 | 
			
		||||
        CoreAriaButtonClickDirective,
 | 
			
		||||
        CoreOnAppearDirective,
 | 
			
		||||
        CoreOnResizeDirective,
 | 
			
		||||
        CoreDownloadFileDirective,
 | 
			
		||||
        CoreCollapsibleHeaderDirective,
 | 
			
		||||
@ -79,6 +78,7 @@ import { CoreUpdateNonReactiveAttributesDirective } from './update-non-reactive-
 | 
			
		||||
        CoreCollapsibleFooterDirective,
 | 
			
		||||
        CoreContentDirective,
 | 
			
		||||
        CoreUpdateNonReactiveAttributesDirective,
 | 
			
		||||
        CoreUserTourDirective,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class CoreDirectivesModule {}
 | 
			
		||||
 | 
			
		||||
@ -93,6 +93,7 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
 | 
			
		||||
        await CoreUserTours.showIfPending({
 | 
			
		||||
            id: 'swipe-navigation',
 | 
			
		||||
            component: CoreSwipeNavigationTourComponent,
 | 
			
		||||
            watch: this.element,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,20 +12,22 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
 | 
			
		||||
import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreCancellablePromise } from '@classes/cancellable-promise';
 | 
			
		||||
import { CoreUserTours, CoreUserToursFocusedOptions, CoreUserToursUserTour } from '@features/usertours/services/user-tours';
 | 
			
		||||
import { CoreDom } from '@singletons/dom';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Directive to listen when an element becomes visible.
 | 
			
		||||
 * Directive to control a User Tour linked to the lifecycle of the element where it's defined.
 | 
			
		||||
 */
 | 
			
		||||
@Directive({
 | 
			
		||||
    selector: '[onAppear]',
 | 
			
		||||
    selector: '[userTour]',
 | 
			
		||||
})
 | 
			
		||||
export class CoreOnAppearDirective implements OnInit, OnDestroy {
 | 
			
		||||
export class CoreUserTourDirective implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @Output() onAppear = new EventEmitter();
 | 
			
		||||
    @Input() userTour!: CoreUserTourDirectiveOptions;
 | 
			
		||||
 | 
			
		||||
    private tour?: CoreUserToursUserTour | null;
 | 
			
		||||
    private element: HTMLElement;
 | 
			
		||||
    protected visiblePromise?: CoreCancellablePromise<void>;
 | 
			
		||||
 | 
			
		||||
@ -41,14 +43,35 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        await this.visiblePromise;
 | 
			
		||||
 | 
			
		||||
        this.onAppear.emit();
 | 
			
		||||
        const { getFocusedElement, ...options } = this.userTour;
 | 
			
		||||
 | 
			
		||||
        this.tour = await CoreUserTours.showIfPending({
 | 
			
		||||
            ...options,
 | 
			
		||||
            focus: getFocusedElement?.(this.element) ?? this.element,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.tour?.cancel();
 | 
			
		||||
        this.visiblePromise?.cancel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * User Tour options to control with this directive.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreUserTourDirectiveOptions = Omit<CoreUserToursFocusedOptions, 'focus'> & {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Getter to obtain element to focus in the User Tour. If this isn't provided, the element where the
 | 
			
		||||
     * directive is defined will be used.
 | 
			
		||||
     *
 | 
			
		||||
     * @param element Element where the directive is defined.
 | 
			
		||||
     */
 | 
			
		||||
    getFocusedElement?(element: HTMLElement): HTMLElement;
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
<ion-button (click)="openBlocks()" (onAppear)="showTour()" [attr.aria-label]="'core.block.opendrawerblocks' | translate" color="secondary"
 | 
			
		||||
<ion-button (click)="openBlocks()" [userTour]="userTour" [attr.aria-label]="'core.block.opendrawerblocks' | translate" color="secondary"
 | 
			
		||||
    #button>
 | 
			
		||||
    <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
</ion-button>
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,8 @@
 | 
			
		||||
 | 
			
		||||
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { CoreCancellablePromise } from '@classes/cancellable-promise';
 | 
			
		||||
import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours';
 | 
			
		||||
import { CoreUserTourDirectiveOptions } from '@directives/user-tour';
 | 
			
		||||
import { CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreDom } from '@singletons/dom';
 | 
			
		||||
import { CoreBlockSideBlocksTourComponent } from '../side-blocks-tour/side-blocks-tour';
 | 
			
		||||
@ -34,6 +35,14 @@ export class CoreBlockSideBlocksButtonComponent implements OnInit, OnDestroy {
 | 
			
		||||
    @Input() instanceId!: number;
 | 
			
		||||
    @ViewChild('button', { read: ElementRef }) button?: ElementRef<HTMLElement>;
 | 
			
		||||
 | 
			
		||||
    userTour: CoreUserTourDirectiveOptions = {
 | 
			
		||||
        id: 'side-blocks-button',
 | 
			
		||||
        component: CoreBlockSideBlocksTourComponent,
 | 
			
		||||
        side: CoreUserToursSide.Start,
 | 
			
		||||
        alignment: CoreUserToursAlignment.Center,
 | 
			
		||||
        getFocusedElement: nativeButton => nativeButton.shadowRoot?.children[0] as HTMLElement,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    protected element: HTMLElement;
 | 
			
		||||
    protected slotPromise?: CoreCancellablePromise<void>;
 | 
			
		||||
 | 
			
		||||
@ -61,25 +70,6 @@ export class CoreBlockSideBlocksButtonComponent implements OnInit, OnDestroy {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show User Tour.
 | 
			
		||||
     */
 | 
			
		||||
    async showTour(): Promise<void> {
 | 
			
		||||
        const nativeButton = this.button?.nativeElement.shadowRoot?.children[0] as HTMLElement;
 | 
			
		||||
 | 
			
		||||
        if (!nativeButton) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUserTours.showIfPending({
 | 
			
		||||
            id: 'side-blocks-button',
 | 
			
		||||
            component: CoreBlockSideBlocksTourComponent,
 | 
			
		||||
            focus: nativeButton,
 | 
			
		||||
            side: CoreUserToursSide.Start,
 | 
			
		||||
            alignment: CoreUserToursAlignment.Center,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,7 @@
 | 
			
		||||
 | 
			
		||||
<!-- Course Index button. -->
 | 
			
		||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && displayCourseIndex">
 | 
			
		||||
    <ion-fab-button (click)="openCourseIndex()" (onAppear)="showCourseIndexTour()" [attr.aria-label]="'core.course.courseindex' | translate"
 | 
			
		||||
    <ion-fab-button (click)="openCourseIndex()" [userTour]="courseIndexTour" [attr.aria-label]="'core.course.courseindex' | translate"
 | 
			
		||||
        color="secondary" #courseIndexFab>
 | 
			
		||||
        <ion-icon name="fas-list-ul" aria-hidden="true"></ion-icon>
 | 
			
		||||
        <span class="sr-only">{{'core.course.courseindex' | translate }}</span>
 | 
			
		||||
 | 
			
		||||
@ -46,9 +46,10 @@ import { CoreBlockHelper } from '@features/block/services/block-helper';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
			
		||||
import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course';
 | 
			
		||||
import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours';
 | 
			
		||||
import { CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours';
 | 
			
		||||
import { CoreCourseCourseIndexTourComponent } from '../course-index-tour/course-index-tour';
 | 
			
		||||
import { CoreDom } from '@singletons/dom';
 | 
			
		||||
import { CoreUserTourDirectiveOptions } from '@directives/user-tour';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component to display course contents using a certain format. If the format isn't found, use default one.
 | 
			
		||||
@ -87,6 +88,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    canLoadMore = false;
 | 
			
		||||
    showSectionId = 0;
 | 
			
		||||
    data: Record<string, unknown> = {}; // Data to pass to the components.
 | 
			
		||||
    courseIndexTour: CoreUserTourDirectiveOptions = {
 | 
			
		||||
        id: 'course-index',
 | 
			
		||||
        component: CoreCourseCourseIndexTourComponent,
 | 
			
		||||
        side: CoreUserToursSide.Top,
 | 
			
		||||
        alignment: CoreUserToursAlignment.End,
 | 
			
		||||
        getFocusedElement: nativeButton => nativeButton.shadowRoot?.children[0] as HTMLElement,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    displayCourseIndex = false;
 | 
			
		||||
    displayBlocks = false;
 | 
			
		||||
@ -166,25 +174,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show Course Index User Tour.
 | 
			
		||||
     */
 | 
			
		||||
    async showCourseIndexTour(): Promise<void> {
 | 
			
		||||
        const nativeButton = this.courseIndexFab?.nativeElement.shadowRoot?.children[0] as HTMLElement;
 | 
			
		||||
 | 
			
		||||
        if (!nativeButton) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUserTours.showIfPending({
 | 
			
		||||
            id: 'course-index',
 | 
			
		||||
            component: CoreCourseCourseIndexTourComponent,
 | 
			
		||||
            focus: nativeButton,
 | 
			
		||||
            side: CoreUserToursSide.Top,
 | 
			
		||||
            alignment: CoreUserToursAlignment.End,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Detect changes on input properties.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
<core-user-avatar *ngIf="isMainScreen && siteInfo" [user]="siteInfo" class="core-bar-button-image clickable" [linkProfile]="false"
 | 
			
		||||
    (ariaButtonClick)="openUserMenu($event)" (onAppear)="showTour()" role="button" tabindex="0"
 | 
			
		||||
    (ariaButtonClick)="openUserMenu($event)" [userTour]="userTour" role="button" tabindex="0"
 | 
			
		||||
    [attr.aria-label]="'core.user.useraccount' | translate" #avatar>
 | 
			
		||||
</core-user-avatar>
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,8 @@
 | 
			
		||||
 | 
			
		||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { CoreSiteInfo } from '@classes/site';
 | 
			
		||||
import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours';
 | 
			
		||||
import { CoreUserTourDirectiveOptions } from '@directives/user-tour';
 | 
			
		||||
import { CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours';
 | 
			
		||||
import { IonRouterOutlet } from '@ionic/angular';
 | 
			
		||||
import { CoreScreen } from '@services/screen';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
@ -36,6 +37,12 @@ export class CoreMainMenuUserButtonComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    siteInfo?: CoreSiteInfo;
 | 
			
		||||
    isMainScreen = false;
 | 
			
		||||
    userTour: CoreUserTourDirectiveOptions = {
 | 
			
		||||
        id: 'user-menu',
 | 
			
		||||
        component: CoreMainMenuUserMenuTourComponent,
 | 
			
		||||
        alignment: CoreUserToursAlignment.Start,
 | 
			
		||||
        side: CoreScreen.isMobile ? CoreUserToursSide.Start : CoreUserToursSide.End,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    @ViewChild('avatar', { read: ElementRef }) avatar?: ElementRef<HTMLElement>;
 | 
			
		||||
 | 
			
		||||
@ -66,21 +73,4 @@ export class CoreMainMenuUserButtonComponent implements OnInit {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show User Tour.
 | 
			
		||||
     */
 | 
			
		||||
    async showTour(): Promise<void> {
 | 
			
		||||
        if (!this.avatar) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUserTours.showIfPending({
 | 
			
		||||
            id: 'user-menu',
 | 
			
		||||
            component: CoreMainMenuUserMenuTourComponent,
 | 
			
		||||
            focus: this.avatar.nativeElement,
 | 
			
		||||
            alignment: CoreUserToursAlignment.Start,
 | 
			
		||||
            side: CoreScreen.isMobile ? CoreUserToursSide.Start : CoreUserToursSide.End,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -55,6 +55,7 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
 | 
			
		||||
    popoverWrapperStyles?: string;
 | 
			
		||||
    popoverWrapperArrowStyles?: string;
 | 
			
		||||
    private element: HTMLElement;
 | 
			
		||||
    private tour?: HTMLElement;
 | 
			
		||||
    private wrapperTransform = '';
 | 
			
		||||
    private wrapperElement = new CorePromisedValue<HTMLElement>();
 | 
			
		||||
    private backButtonListener?: (event: BackButtonEvent) => void;
 | 
			
		||||
@ -77,14 +78,18 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Present User Tour.
 | 
			
		||||
     * Show User Tour.
 | 
			
		||||
     */
 | 
			
		||||
    async present(): Promise<void> {
 | 
			
		||||
    async show(): Promise<void> {
 | 
			
		||||
        // Insert tour component and wait until it's ready.
 | 
			
		||||
        const wrapper = await this.wrapperElement;
 | 
			
		||||
        const tour = await AngularFrameworkDelegate.attachViewToDom(wrapper, this.component, this.componentProps ?? {});
 | 
			
		||||
        this.tour = await AngularFrameworkDelegate.attachViewToDom(wrapper, this.component, this.componentProps ?? {});
 | 
			
		||||
 | 
			
		||||
        await CoreDomUtils.waitForImages(tour);
 | 
			
		||||
        if (!this.tour) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreDomUtils.waitForImages(this.tour);
 | 
			
		||||
 | 
			
		||||
        // Calculate focus styles or dismiss if the element is gone.
 | 
			
		||||
        if (this.focus && !CoreDom.isElementVisible(this.focus)) {
 | 
			
		||||
@ -111,6 +116,19 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
 | 
			
		||||
        await this.playEnterAnimation();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Hide User Tour temporarily.
 | 
			
		||||
     */
 | 
			
		||||
    async hide(): Promise<void> {
 | 
			
		||||
        const wrapper = await this.wrapperElement;
 | 
			
		||||
 | 
			
		||||
        await this.playLeaveAnimation();
 | 
			
		||||
        await AngularFrameworkDelegate.removeViewFromDom(wrapper, this.tour);
 | 
			
		||||
 | 
			
		||||
        this.active = false;
 | 
			
		||||
        this.backButtonListener && document.removeEventListener('ionBackButton', this.backButtonListener);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Dismiss User Tour.
 | 
			
		||||
     *
 | 
			
		||||
@ -119,13 +137,15 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
 | 
			
		||||
    async dismiss(acknowledge: boolean = true): Promise<void> {
 | 
			
		||||
        this.beforeDismiss.emit();
 | 
			
		||||
 | 
			
		||||
        await this.playLeaveAnimation();
 | 
			
		||||
        await Promise.all<unknown>([
 | 
			
		||||
            AngularFrameworkDelegate.removeViewFromDom(this.container, this.element),
 | 
			
		||||
            acknowledge && CoreUserTours.acknowledge(this.id),
 | 
			
		||||
        ]);
 | 
			
		||||
        if (this.active) {
 | 
			
		||||
            await this.playLeaveAnimation();
 | 
			
		||||
            await AngularFrameworkDelegate.removeViewFromDom(this.container, this.element);
 | 
			
		||||
 | 
			
		||||
            this.backButtonListener && document.removeEventListener('ionBackButton', this.backButtonListener);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        acknowledge && await CoreUserTours.acknowledge(this.id);
 | 
			
		||||
 | 
			
		||||
        this.backButtonListener && document.removeEventListener('ionBackButton', this.backButtonListener);
 | 
			
		||||
        this.afterDismiss.emit();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,12 +15,14 @@
 | 
			
		||||
import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { asyncInstance } from '@/core/utils/async-instance';
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreCancellablePromise } from '@classes/cancellable-promise';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database/database-table';
 | 
			
		||||
import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { AngularFrameworkDelegate, makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
 | 
			
		||||
import { CoreDom } from '@singletons/dom';
 | 
			
		||||
import { CoreSubscriptions } from '@singletons/subscriptions';
 | 
			
		||||
import { CoreUserToursUserTourComponent } from '../components/user-tour/user-tour';
 | 
			
		||||
import { APP_SCHEMA, CoreUserToursDBEntry, USER_TOURS_TABLE_NAME } from './database/user-tours';
 | 
			
		||||
@ -32,8 +34,7 @@ import { APP_SCHEMA, CoreUserToursDBEntry, USER_TOURS_TABLE_NAME } from './datab
 | 
			
		||||
export class CoreUserToursService {
 | 
			
		||||
 | 
			
		||||
    protected table = asyncInstance<CoreDatabaseTable<CoreUserToursDBEntry>>();
 | 
			
		||||
    protected tours: CoreUserToursUserTourComponent[] = [];
 | 
			
		||||
    protected tourReadyCallbacks = new WeakMap<CoreUserToursUserTourComponent, () => void>();
 | 
			
		||||
    protected tours: { component: CoreUserToursUserTourComponent; visible: boolean }[] = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize database.
 | 
			
		||||
@ -84,14 +85,17 @@ export class CoreUserToursService {
 | 
			
		||||
     * Show a User Tour if it's pending.
 | 
			
		||||
     *
 | 
			
		||||
     * @param options User Tour options.
 | 
			
		||||
     * @returns User tour controller, if any.
 | 
			
		||||
     */
 | 
			
		||||
    async showIfPending(options: CoreUserToursBasicOptions): Promise<void>;
 | 
			
		||||
    async showIfPending(options: CoreUserToursFocusedOptions): Promise<void>;
 | 
			
		||||
    async showIfPending(options: CoreUserToursBasicOptions | CoreUserToursFocusedOptions): Promise<void> {
 | 
			
		||||
    async showIfPending(options: CoreUserToursBasicOptions): Promise<CoreUserToursUserTour | null>;
 | 
			
		||||
    async showIfPending(options: CoreUserToursFocusedOptions): Promise<CoreUserToursUserTour | null>;
 | 
			
		||||
    async showIfPending(
 | 
			
		||||
        options: CoreUserToursBasicOptions | CoreUserToursFocusedOptions,
 | 
			
		||||
    ): Promise<CoreUserToursUserTour | null> {
 | 
			
		||||
        const isPending = await CoreUserTours.isPending(options.id);
 | 
			
		||||
 | 
			
		||||
        if (!isPending) {
 | 
			
		||||
            return;
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.show(options);
 | 
			
		||||
@ -101,16 +105,15 @@ export class CoreUserToursService {
 | 
			
		||||
     * Show a User Tour.
 | 
			
		||||
     *
 | 
			
		||||
     * @param options User Tour options.
 | 
			
		||||
     * @returns User tour controller.
 | 
			
		||||
     */
 | 
			
		||||
    protected async show(options: CoreUserToursBasicOptions): Promise<void>;
 | 
			
		||||
    protected async show(options: CoreUserToursFocusedOptions): Promise<void>;
 | 
			
		||||
    protected async show(options: CoreUserToursBasicOptions | CoreUserToursFocusedOptions): Promise<void> {
 | 
			
		||||
    protected async show(options: CoreUserToursBasicOptions): Promise<CoreUserToursUserTour>;
 | 
			
		||||
    protected async show(options: CoreUserToursFocusedOptions): Promise<CoreUserToursUserTour>;
 | 
			
		||||
    protected async show(options: CoreUserToursBasicOptions | CoreUserToursFocusedOptions): Promise<CoreUserToursUserTour> {
 | 
			
		||||
        const { delay, ...componentOptions } = options;
 | 
			
		||||
 | 
			
		||||
        // Delay start.
 | 
			
		||||
        await CoreUtils.wait(delay ?? 200);
 | 
			
		||||
 | 
			
		||||
        // Create tour.
 | 
			
		||||
        const container = document.querySelector('ion-app') ?? document.body;
 | 
			
		||||
        const element = await AngularFrameworkDelegate.attachViewToDom(
 | 
			
		||||
            container,
 | 
			
		||||
@ -119,26 +122,7 @@ export class CoreUserToursService {
 | 
			
		||||
        );
 | 
			
		||||
        const tour = CoreComponentsRegistry.require(element, CoreUserToursUserTourComponent);
 | 
			
		||||
 | 
			
		||||
        this.tours.push(tour);
 | 
			
		||||
 | 
			
		||||
        // Handle present/dismiss lifecycle.
 | 
			
		||||
        CoreSubscriptions.once(tour.beforeDismiss, () => {
 | 
			
		||||
            const index = this.tours.indexOf(tour);
 | 
			
		||||
 | 
			
		||||
            if (index === -1) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.tours.splice(index, 1);
 | 
			
		||||
 | 
			
		||||
            const nextTour = this.tours[0] as CoreUserToursUserTourComponent | undefined;
 | 
			
		||||
 | 
			
		||||
            nextTour?.present().then(() => this.tourReadyCallbacks.get(nextTour)?.());
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.tours.length > 1
 | 
			
		||||
            ? await new Promise<void>(resolve => this.tourReadyCallbacks.set(tour, resolve))
 | 
			
		||||
            : await tour.present();
 | 
			
		||||
        return this.startTour(tour, options.watch ?? (options as CoreUserToursFocusedOptions).focus);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -147,13 +131,144 @@ 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[0]?.dismiss(acknowledge);
 | 
			
		||||
        await this.tours.find(({ visible }) => visible)?.component.dismiss(acknowledge);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Activate a tour component and bind its lifecycle to an element if provided.
 | 
			
		||||
     *
 | 
			
		||||
     * @param tour User tour.
 | 
			
		||||
     * @param watchElement Element to watch in order to update tour lifecycle.
 | 
			
		||||
     * @returns User tour controller.
 | 
			
		||||
     */
 | 
			
		||||
    protected startTour(tour: CoreUserToursUserTourComponent, watchElement?: HTMLElement | false): CoreUserToursUserTour {
 | 
			
		||||
        if (!watchElement) {
 | 
			
		||||
            this.activateTour(tour);
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                cancel: () => tour.dismiss(false),
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let unsubscribeVisible: (() => void) | undefined;
 | 
			
		||||
        let visiblePromise: CoreCancellablePromise | undefined = CoreDom.waitToBeInViewport(watchElement);
 | 
			
		||||
 | 
			
		||||
        // eslint-disable-next-line promise/catch-or-return, promise/always-return
 | 
			
		||||
        visiblePromise.then(() => {
 | 
			
		||||
            visiblePromise = undefined;
 | 
			
		||||
 | 
			
		||||
            this.activateTour(tour);
 | 
			
		||||
 | 
			
		||||
            unsubscribeVisible = CoreDom.watchElementInViewport(
 | 
			
		||||
                watchElement,
 | 
			
		||||
                visible => visible ? this.activateTour(tour) : this.deactivateTour(tour),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            CoreSubscriptions.once(tour.beforeDismiss, () => {
 | 
			
		||||
                unsubscribeVisible?.();
 | 
			
		||||
 | 
			
		||||
                visiblePromise = undefined;
 | 
			
		||||
                unsubscribeVisible = undefined;
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            cancel: async () => {
 | 
			
		||||
                visiblePromise?.cancel();
 | 
			
		||||
 | 
			
		||||
                if (!unsubscribeVisible) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                unsubscribeVisible();
 | 
			
		||||
 | 
			
		||||
                await tour.dismiss(false);
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Activate the given user tour.
 | 
			
		||||
     *
 | 
			
		||||
     * @param tour User tour.
 | 
			
		||||
     */
 | 
			
		||||
    protected activateTour(tour: CoreUserToursUserTourComponent): void {
 | 
			
		||||
        // Handle show/dismiss lifecycle.
 | 
			
		||||
        CoreSubscriptions.once(tour.beforeDismiss, () => {
 | 
			
		||||
            const index = this.tours.findIndex(({ component }) => component === tour);
 | 
			
		||||
 | 
			
		||||
            if (index === -1) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.tours.splice(index, 1);
 | 
			
		||||
 | 
			
		||||
            const foregroundTour = this.tours.find(({ visible }) => visible);
 | 
			
		||||
 | 
			
		||||
            foregroundTour?.component.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);
 | 
			
		||||
 | 
			
		||||
        if (index !== -1) {
 | 
			
		||||
            this.tours[index].visible = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.tours.push({
 | 
			
		||||
                visible: true,
 | 
			
		||||
                component: tour,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.tours.find(({ visible }) => visible)?.component !== tour) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foregroundTour?.component.hide();
 | 
			
		||||
 | 
			
		||||
        tour.show();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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);
 | 
			
		||||
 | 
			
		||||
        if (index === -1) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.tours[index].visible = false;
 | 
			
		||||
 | 
			
		||||
        if (index === foregroundTourIndex) {
 | 
			
		||||
            tour.hide();
 | 
			
		||||
 | 
			
		||||
            this.tours.find(({ visible }) => visible)?.component.show();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CoreUserTours = makeSingleton(CoreUserToursService);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * User Tour controller.
 | 
			
		||||
 */
 | 
			
		||||
export interface CoreUserToursUserTour {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Cancelling a User Tours removed it from the queue if it was pending or dimisses it without
 | 
			
		||||
     * acknowledging if it existed.
 | 
			
		||||
     */
 | 
			
		||||
    cancel(): Promise<void>;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * User Tour side.
 | 
			
		||||
 */
 | 
			
		||||
@ -202,6 +317,13 @@ export interface CoreUserToursBasicOptions {
 | 
			
		||||
     */
 | 
			
		||||
    delay?: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether to watch an element to bind the User Tour lifecycle. Whenever this element appears or
 | 
			
		||||
     * leaves the screen, the user tour will do it as well. Focused user tours do it by default with
 | 
			
		||||
     * the focused element, but it can be disabled by explicitly using `false` here.
 | 
			
		||||
     */
 | 
			
		||||
    watch?: HTMLElement | false;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -354,6 +354,62 @@ export class CoreDom {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Watch whenever an elements visibility changes within the viewport.
 | 
			
		||||
     *
 | 
			
		||||
     * @param element Element to watch.
 | 
			
		||||
     * @param intersectionRatio Intersection ratio (From 0 to 1).
 | 
			
		||||
     * @param callback Callback when visibility changes.
 | 
			
		||||
     * @return Function to stop watching.
 | 
			
		||||
     */
 | 
			
		||||
    static watchElementInViewport(
 | 
			
		||||
        element: HTMLElement,
 | 
			
		||||
        intersectionRatio: number,
 | 
			
		||||
        callback: (visible: boolean) => void,
 | 
			
		||||
    ): () => void;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Watch whenever an elements visibility changes within the viewport.
 | 
			
		||||
     *
 | 
			
		||||
     * @param element Element to watch.
 | 
			
		||||
     * @param callback Callback when visibility changes.
 | 
			
		||||
     * @return Function to stop watching.
 | 
			
		||||
     */
 | 
			
		||||
    static watchElementInViewport(element: HTMLElement, callback: (visible: boolean) => void): () => void;
 | 
			
		||||
 | 
			
		||||
    static watchElementInViewport(
 | 
			
		||||
        element: HTMLElement,
 | 
			
		||||
        intersectionRatioOrCallback: number | ((visible: boolean) => void),
 | 
			
		||||
        callback?: (visible: boolean) => void,
 | 
			
		||||
    ): () => void {
 | 
			
		||||
        const visibleCallback = callback ?? intersectionRatioOrCallback as (visible: boolean) => void;
 | 
			
		||||
        const intersectionRatio = typeof intersectionRatioOrCallback === 'number' ? intersectionRatioOrCallback : 1;
 | 
			
		||||
 | 
			
		||||
        let visible = CoreDom.isElementInViewport(element, intersectionRatio);
 | 
			
		||||
        const setVisible = (newValue: boolean) => {
 | 
			
		||||
            if (visible === newValue) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            visible = newValue;
 | 
			
		||||
            visibleCallback(visible);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (!('IntersectionObserver' in window)) {
 | 
			
		||||
            const interval = setInterval(() => setVisible(CoreDom.isElementInViewport(element, intersectionRatio)), 50);
 | 
			
		||||
 | 
			
		||||
            return () => clearInterval(interval);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const observer = new IntersectionObserver(([{ isIntersecting, intersectionRatio }]) => {
 | 
			
		||||
            setVisible(isIntersecting && intersectionRatio >= intersectionRatio);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        observer.observe(element);
 | 
			
		||||
 | 
			
		||||
        return () => observer.disconnect();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Wait an element to be in dom and visible.
 | 
			
		||||
     *
 | 
			
		||||
@ -362,48 +418,29 @@ export class CoreDom {
 | 
			
		||||
     * @return Cancellable promise.
 | 
			
		||||
     */
 | 
			
		||||
    static waitToBeInViewport(element: HTMLElement, intersectionRatio = 1): CoreCancellablePromise<void> {
 | 
			
		||||
        let unsubscribe: (() => void) | undefined;
 | 
			
		||||
        const visiblePromise = CoreDom.waitToBeVisible(element);
 | 
			
		||||
 | 
			
		||||
        let intersectionObserver: IntersectionObserver;
 | 
			
		||||
        let interval: number | undefined;
 | 
			
		||||
 | 
			
		||||
        return new CoreCancellablePromise<void>(
 | 
			
		||||
            async (resolve) => {
 | 
			
		||||
                await visiblePromise;
 | 
			
		||||
 | 
			
		||||
                if (CoreDom.isElementInViewport(element, intersectionRatio)) {
 | 
			
		||||
 | 
			
		||||
                    return resolve();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if ('IntersectionObserver' in window) {
 | 
			
		||||
                    intersectionObserver = new IntersectionObserver((observerEntries) => {
 | 
			
		||||
                        const isIntersecting = observerEntries
 | 
			
		||||
                            .some((entry) => entry.isIntersecting && entry.intersectionRatio >= intersectionRatio);
 | 
			
		||||
                        if (!isIntersecting) {
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                unsubscribe = this.watchElementInViewport(element, intersectionRatio, inViewport => {
 | 
			
		||||
                    if (!inViewport) {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                        resolve();
 | 
			
		||||
                        intersectionObserver?.disconnect();
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    intersectionObserver.observe(element);
 | 
			
		||||
                } else {
 | 
			
		||||
                    interval = window.setInterval(() => {
 | 
			
		||||
                        if (!CoreDom.isElementInViewport(element, intersectionRatio)) {
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        resolve();
 | 
			
		||||
                        window.clearInterval(interval);
 | 
			
		||||
                    }, 50);
 | 
			
		||||
                }
 | 
			
		||||
                    resolve();
 | 
			
		||||
                    unsubscribe?.();
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            () => {
 | 
			
		||||
                visiblePromise.cancel();
 | 
			
		||||
                intersectionObserver?.disconnect();
 | 
			
		||||
                window.clearInterval(interval);
 | 
			
		||||
                unsubscribe?.();
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user