import { Overlay, OverlayOutsideClickDispatcher, FlexibleConnectedPositionStrategy } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import * as i0 from '@angular/core';
import { InjectionToken, inject, signal, ChangeDetectionStrategy, Component, ElementRef, viewChild, DOCUMENT, HostListener, Injector, Injectable } from '@angular/core';
import { isRTL, calculateOverlayArrowPosition } from '@siemens/element-ng/common';
import { ResizeObserverService } from '@siemens/element-ng/resize-observer';
import { Subject, map, merge, throttleTime, tap } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import * as i1 from '@angular/cdk/a11y';
import { A11yModule } from '@angular/cdk/a11y';
import { elementCancel } from '@siemens/element-icons';
import { addIcons, SiIconComponent } from '@siemens/element-ng/icon';
import { t, SiTranslatePipe } from '@siemens/element-translate-ng/translate';

const SI_TOUR_TOKEN = new InjectionToken('SI_TOUR_TOKEN');

/**
 * Copyright (c) Siemens 2016 - 2026
 * SPDX-License-Identifier: MIT
 */
class SiTourHighlightComponent {
    tourToken = inject(SI_TOUR_TOKEN);
    anchor;
    dim = signal({ top: 0, left: 0, width: 0, height: 0 }, ...(ngDevMode ? [{ debugName: "dim" }] : []));
    constructor() {
        this.tourToken.currentStep.pipe(takeUntilDestroyed()).subscribe(step => {
            this.anchor = step.anchor;
            this.updateHighlight();
        });
        this.tourToken.sizeChange.pipe(takeUntilDestroyed()).subscribe(() => this.updateHighlight());
    }
    updateHighlight() {
        if (!this.anchor) {
            this.dim.set({ top: 0, left: 0, width: 0, height: 0 });
            return;
        }
        const rect = this.anchor.nativeElement.getBoundingClientRect();
        this.dim.set({ top: rect.top, left: rect.left, width: rect.width, height: rect.height });
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: SiTourHighlightComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
    static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.8", type: SiTourHighlightComponent, isStandalone: true, selector: "si-tour-highlight", ngImport: i0, template: "@let rect = dim();\n<div\n  class=\"highlighter\"\n  [style.top.px]=\"rect.top\"\n  [style.left.px]=\"rect.left\"\n  [style.inline-size.px]=\"rect.width\"\n  [style.block-size.px]=\"rect.height\"\n></div>\n", styles: [".highlighter{position:fixed;box-shadow:var(--element-base-translucent-1) 0 0 0 1000vmax;pointer-events:none}::ng-deep .cdk-global-overlay-wrapper:has(si-tour-highlight){z-index:2000}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: SiTourHighlightComponent, decorators: [{
            type: Component,
            args: [{ selector: 'si-tour-highlight', changeDetection: ChangeDetectionStrategy.OnPush, template: "@let rect = dim();\n<div\n  class=\"highlighter\"\n  [style.top.px]=\"rect.top\"\n  [style.left.px]=\"rect.left\"\n  [style.inline-size.px]=\"rect.width\"\n  [style.block-size.px]=\"rect.height\"\n></div>\n", styles: [".highlighter{position:fixed;box-shadow:var(--element-base-translucent-1) 0 0 0 1000vmax;pointer-events:none}::ng-deep .cdk-global-overlay-wrapper:has(si-tour-highlight){z-index:2000}\n"] }]
        }], ctorParameters: () => [] });

/**
 * Copyright (c) Siemens 2016 - 2026
 * SPDX-License-Identifier: MIT
 */
class SiTourComponent {
    positionClass = signal('', ...(ngDevMode ? [{ debugName: "positionClass" }] : []));
    arrowPos = signal(undefined, ...(ngDevMode ? [{ debugName: "arrowPos" }] : []));
    step = signal(undefined, ...(ngDevMode ? [{ debugName: "step" }] : []));
    show = signal(false, ...(ngDevMode ? [{ debugName: "show" }] : []));
    icons = addIcons({ elementCancel });
    tourToken = inject(SI_TOUR_TOKEN);
    backText = t(() => $localize `:@@SI_TOUR.BACK:Back`);
    nextText = t(() => $localize `:@@SI_TOUR.NEXT:Next`);
    skipText = t(() => $localize `:@@SI_TOUR.SKIP:Skip tour`);
    doneText = t(() => $localize `:@@SI_TOUR.DONE:Done`);
    ariaLabelClose = t(() => $localize `:@@SI_TOUR.CLOSE:Close`);
    progressText = t(() => $localize `:@@SI_TOUR.PROGRESS: {{step}} of {{total}}`);
    elementRef = inject(ElementRef);
    subscription;
    prevFocus = null;
    focusTrap = viewChild('focusTrap', ...(ngDevMode ? [{ debugName: "focusTrap" }] : []));
    document = inject(DOCUMENT);
    constructor() {
        this.subscription = this.tourToken.currentStep.subscribe(step => {
            this.step.set(step);
            this.show.set(false);
            // prevents flickering during overlay reposition and arrow direction change
            setTimeout(() => {
                this.show.set(true);
                this.ensureFocused();
            }, 50);
        });
        this.subscription.add(this.tourToken.positionChange.subscribe(update => this.updatePosition(update)));
    }
    ngOnDestroy() {
        this.subscription?.unsubscribe();
    }
    action(action) {
        this.prevFocus = this.document.activeElement;
        this.tourToken.control.next(action);
    }
    ensureFocused() {
        // ensure focus is inside si-tour as other element might steal it, e.g. opening a menu
        const element = this.elementRef.nativeElement;
        if (!this.document.activeElement || !element.contains(this.document.activeElement)) {
            if (element.contains(this.prevFocus)) {
                this.prevFocus?.focus?.();
            }
            else {
                this.focusTrap()?.focusTrap.focusInitialElement();
            }
        }
    }
    keyListener(event) {
        switch (event.key) {
            case 'Escape':
                this.action('cancel');
                break;
            case 'ArrowLeft':
                this.action(isRTL() ? 'next' : 'back');
                break;
            case 'ArrowRight':
                this.action(isRTL() ? 'back' : 'next');
                break;
        }
    }
    updatePosition(update) {
        if (!update) {
            this.positionClass.set('');
            this.arrowPos.set(undefined);
            return;
        }
        const connPair = update.change.connectionPair;
        this.positionClass.set(`popover-${connPair.overlayX}-${connPair.overlayY}`);
        setTimeout(() => this.arrowPos.set(calculateOverlayArrowPosition(update.change, this.elementRef, update.anchor)));
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: SiTourComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
    static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: SiTourComponent, isStandalone: true, selector: "si-tour", host: { listeners: { "window:keydown": "keyListener($event)" }, properties: { "attr.data-step-id": "step()?.step?.id" } }, viewQueries: [{ propertyName: "focusTrap", first: true, predicate: ["focusTrap"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"backdrop\"></div>\n<div\n  #focusTrap=\"cdkTrapFocus\"\n  role=\"dialog\"\n  aria-labelledby=\"__si-tour-step-title\"\n  cdkTrapFocus\n  cdkTrapFocusAutoCapture\n  [class]=\"['tour-content', 'popover', 'position-relative', positionClass()]\"\n  [class.show]=\"show()\"\n>\n  @if (arrowPos()) {\n    <div\n      class=\"popover-arrow\"\n      [style.left.px]=\"arrowPos()?.left\"\n      [style.right.px]=\"arrowPos()?.right\"\n    ></div>\n  }\n  @if (step()) {\n    @let tourStep = step()!;\n    <div class=\"d-flex\">\n      <div class=\"p-6 flex-fill\">\n        <div class=\"pb-2 si-body text-secondary\">\n          {{ progressText | translate: { step: tourStep.stepNumber, total: tourStep.totalSteps } }}\n        </div>\n        <div id=\"__si-tour-step-title\" class=\"si-h4 mb-0\">\n          {{ tourStep.step.title | translate }}\n        </div>\n      </div>\n      <button\n        type=\"button\"\n        class=\"btn btn-circle btn-ghost mt-4 me-5\"\n        [attr.aria-label]=\"ariaLabelClose | translate\"\n        (click)=\"action('cancel')\"\n      >\n        <si-icon [icon]=\"icons.elementCancel\" />\n      </button>\n    </div>\n    <div class=\"step-text si-body px-6 pb-8\">\n      {{ tourStep.step.text | translate }}\n    </div>\n    <div class=\"d-flex justify-content-end align-items-center p-6 gap-6\">\n      @if (tourStep.stepNumber !== 1) {\n        <button\n          type=\"button\"\n          class=\"btn btn-secondary\"\n          [disabled]=\"tourToken.blocked()\"\n          (click)=\"action('back')\"\n        >\n          {{ backText | translate }}\n        </button>\n      } @else {\n        <button type=\"button\" class=\"btn btn-tertiary\" (click)=\"action('cancel')\">\n          {{ skipText | translate }}\n        </button>\n      }\n      <button\n        type=\"button\"\n        class=\"btn btn-primary\"\n        cdkFocusInitial\n        [disabled]=\"tourToken.blocked()\"\n        (click)=\"action(tourStep.stepNumber !== tourStep.totalSteps ? 'next' : 'complete')\"\n      >\n        @if (tourStep.stepNumber !== tourStep.totalSteps) {\n          {{ nextText | translate }}\n        } @else {\n          {{ doneText | translate }}\n        }\n      </button>\n    </div>\n  }\n</div>\n", styles: [".tour-content{--popover-arrow-width: 24px;--popover-arrow-height: 12px;--popover-bg-color: var(--element-base-1);max-inline-size:400px;inline-size:100vw;opacity:0}.tour-content.show{opacity:1}.backdrop{position:fixed;inset:-200vmax}::ng-deep .cdk-overlay-connected-position-bounding-box:has(si-tour),::ng-deep .cdk-global-overlay-wrapper:has(si-tour){z-index:2000}\n"], dependencies: [{ kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: SiIconComponent, selector: "si-icon", inputs: ["icon"] }, { kind: "pipe", type: SiTranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: SiTourComponent, decorators: [{
            type: Component,
            args: [{ selector: 'si-tour', imports: [A11yModule, SiIconComponent, SiTranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: {
                        '[attr.data-step-id]': 'step()?.step?.id'
                    }, template: "<div class=\"backdrop\"></div>\n<div\n  #focusTrap=\"cdkTrapFocus\"\n  role=\"dialog\"\n  aria-labelledby=\"__si-tour-step-title\"\n  cdkTrapFocus\n  cdkTrapFocusAutoCapture\n  [class]=\"['tour-content', 'popover', 'position-relative', positionClass()]\"\n  [class.show]=\"show()\"\n>\n  @if (arrowPos()) {\n    <div\n      class=\"popover-arrow\"\n      [style.left.px]=\"arrowPos()?.left\"\n      [style.right.px]=\"arrowPos()?.right\"\n    ></div>\n  }\n  @if (step()) {\n    @let tourStep = step()!;\n    <div class=\"d-flex\">\n      <div class=\"p-6 flex-fill\">\n        <div class=\"pb-2 si-body text-secondary\">\n          {{ progressText | translate: { step: tourStep.stepNumber, total: tourStep.totalSteps } }}\n        </div>\n        <div id=\"__si-tour-step-title\" class=\"si-h4 mb-0\">\n          {{ tourStep.step.title | translate }}\n        </div>\n      </div>\n      <button\n        type=\"button\"\n        class=\"btn btn-circle btn-ghost mt-4 me-5\"\n        [attr.aria-label]=\"ariaLabelClose | translate\"\n        (click)=\"action('cancel')\"\n      >\n        <si-icon [icon]=\"icons.elementCancel\" />\n      </button>\n    </div>\n    <div class=\"step-text si-body px-6 pb-8\">\n      {{ tourStep.step.text | translate }}\n    </div>\n    <div class=\"d-flex justify-content-end align-items-center p-6 gap-6\">\n      @if (tourStep.stepNumber !== 1) {\n        <button\n          type=\"button\"\n          class=\"btn btn-secondary\"\n          [disabled]=\"tourToken.blocked()\"\n          (click)=\"action('back')\"\n        >\n          {{ backText | translate }}\n        </button>\n      } @else {\n        <button type=\"button\" class=\"btn btn-tertiary\" (click)=\"action('cancel')\">\n          {{ skipText | translate }}\n        </button>\n      }\n      <button\n        type=\"button\"\n        class=\"btn btn-primary\"\n        cdkFocusInitial\n        [disabled]=\"tourToken.blocked()\"\n        (click)=\"action(tourStep.stepNumber !== tourStep.totalSteps ? 'next' : 'complete')\"\n      >\n        @if (tourStep.stepNumber !== tourStep.totalSteps) {\n          {{ nextText | translate }}\n        } @else {\n          {{ doneText | translate }}\n        }\n      </button>\n    </div>\n  }\n</div>\n", styles: [".tour-content{--popover-arrow-width: 24px;--popover-arrow-height: 12px;--popover-bg-color: var(--element-base-1);max-inline-size:400px;inline-size:100vw;opacity:0}.tour-content.show{opacity:1}.backdrop{position:fixed;inset:-200vmax}::ng-deep .cdk-overlay-connected-position-bounding-box:has(si-tour),::ng-deep .cdk-global-overlay-wrapper:has(si-tour){z-index:2000}\n"] }]
        }], ctorParameters: () => [], propDecorators: { focusTrap: [{ type: i0.ViewChild, args: ['focusTrap', { isSignal: true }] }], keyListener: [{
                type: HostListener,
                args: ['window:keydown', ['$event']]
            }] } });

/**
 * Copyright (c) Siemens 2016 - 2026
 * SPDX-License-Identifier: MIT
 */
class SiTourService {
    injector = inject(Injector);
    resizeObserver = inject(ResizeObserverService);
    overlay = inject(Overlay);
    outsideClickDispatcher = inject(OverlayOutsideClickDispatcher);
    overlayRefHighlight;
    overlayRef;
    portal;
    options = {};
    steps = [];
    currentStepIndex = -1;
    currentStep;
    active = false;
    tourToken = {
        currentStep: new Subject(),
        blocked: signal(false),
        positionChange: new Subject(),
        sizeChange: new Subject(),
        control: new Subject()
    };
    positionChangeSub;
    resizeSub;
    document = inject(DOCUMENT);
    constructor() {
        this.tourToken.control.subscribe(action => this.controlAction(action));
    }
    /**
     * Event triggered after the tour is completed.
     */
    onTourComplete = new Subject();
    /**
     * Event triggered after the tour is cancelled, either through skip button or closing the tour dialog.
     */
    onTourCancel = new Subject();
    /**
     * Sets options for the whole tour.
     */
    setOptions(options) {
        Object.assign(this.options, options);
    }
    /**
     * Add steps to the tour
     */
    addSteps(steps) {
        this.steps.push(...steps);
    }
    /**
     * Clear all steps
     */
    clearSteps() {
        this.steps = [];
        this.currentStepIndex = -1;
    }
    /**
     * Start the tour
     */
    start() {
        this.makeStep(0);
    }
    /**
     * Finish the tour
     */
    complete() {
        this.controlAction('complete');
    }
    async controlAction(action) {
        switch (action) {
            case 'next':
                this.makeStep(this.currentStepIndex + 1);
                break;
            case 'back':
                this.makeStep(this.currentStepIndex - 1);
                break;
            case 'cancel':
                this.hide();
                this.onTourCancel.next();
                break;
            case 'complete':
                this.hide();
                this.onTourComplete.next();
                break;
        }
    }
    async makeStep(index) {
        if (index < 0 || index >= this.steps.length) {
            return;
        }
        this.active = true;
        await this.handleBeforeNextPromise();
        this.currentStepIndex = index;
        const step = this.steps[this.currentStepIndex];
        this.currentStep = Object.assign({}, this.options?.defaultStepOptions ?? {}, step);
        if (this.currentStep.beforeShowPromise) {
            this.tourToken.blocked.set(true);
            await this.currentStep.beforeShowPromise();
        }
        if (!this.active) {
            // tour could have been cancelled before beforeShowPromise completes
            return;
        }
        const anchorElement = this.getElement(step.attachTo?.element);
        this.scrollIntoView(this.currentStep, anchorElement);
        const anchorElementRef = this.isElementVisible(anchorElement)
            ? new ElementRef(anchorElement)
            : undefined;
        if (anchorElementRef) {
            this.attachOverlay(anchorElementRef);
        }
        else {
            this.centerOverlay();
        }
        this.handleResizeSubscription(anchorElementRef);
        this.tourToken.currentStep.next({
            step,
            stepNumber: this.currentStepIndex + 1,
            totalSteps: this.steps.length,
            anchor: anchorElementRef
        });
        this.tourToken.blocked.set(false);
    }
    scrollIntoView(step, element) {
        if (!element || !(step.scrollTo || step.scrollToHandler)) {
            return;
        }
        if (step.scrollToHandler) {
            step.scrollToHandler(element);
        }
        else {
            element?.scrollIntoView(step.scrollTo);
        }
    }
    async handleBeforeNextPromise() {
        if (!this.active) {
            return;
        }
        if (this.currentStep?.beforeNextPromise) {
            this.tourToken.blocked.set(true);
            await this.currentStep.beforeNextPromise();
            this.tourToken.blocked.set(false);
        }
    }
    centerOverlay() {
        this.positionChangeSub?.unsubscribe();
        this.createOrUpdateOverlays();
        const positionStrategy = this.overlayRef.getConfig().positionStrategy;
        if (!positionStrategy || positionStrategy instanceof FlexibleConnectedPositionStrategy) {
            this.overlayRef.updatePositionStrategy(this.overlay.position().global().centerHorizontally().centerVertically());
        }
        this.tourToken.positionChange.next(undefined);
    }
    attachOverlay(anchor) {
        this.createOrUpdateOverlays();
        const positionStrategy = this.overlayRef.getConfig().positionStrategy;
        if (positionStrategy && positionStrategy instanceof FlexibleConnectedPositionStrategy) {
            positionStrategy.setOrigin(anchor);
        }
        else {
            this.createFlexiblePositionStrategy(anchor);
        }
    }
    createFlexiblePositionStrategy(anchor) {
        const positionStrategy = this.overlay
            .position()
            .flexibleConnectedTo(anchor)
            .withGrowAfterOpen(true)
            .withPositions([
            // On top
            { originX: 'center', overlayX: 'center', originY: 'top', overlayY: 'bottom' },
            { originX: 'start', overlayX: 'start', originY: 'top', overlayY: 'bottom' },
            { originX: 'end', overlayX: 'end', originY: 'top', overlayY: 'bottom' },
            // On bottom
            { originX: 'center', overlayX: 'center', originY: 'bottom', overlayY: 'top' },
            { originX: 'start', overlayX: 'start', originY: 'bottom', overlayY: 'top' },
            { originX: 'end', overlayX: 'end', originY: 'bottom', overlayY: 'top' },
            // Left and right
            { originX: 'start', overlayX: 'end', originY: 'center', overlayY: 'center' },
            { originX: 'end', overlayX: 'start', originY: 'center', overlayY: 'center' }
        ]);
        this.overlayRef.updatePositionStrategy(positionStrategy);
        this.positionChangeSub = positionStrategy.positionChanges
            .pipe(map(change => ({ change, anchor })))
            // We only want to forward the next channel, as the positionChanges completes when setting a new origin.
            .subscribe(value => this.tourToken.positionChange.next(value));
    }
    createOrUpdateOverlays() {
        if (this.overlayRef) {
            // This moves the dispatcher to the top, allowing it to catch other open overlays.
            // Much lighter than to detach and re-attach the portal, not re-creating the si-tour
            // component for each step
            this.outsideClickDispatcher.remove(this.overlayRef);
            this.outsideClickDispatcher.add(this.overlayRef);
            return;
        }
        const componentInjector = Injector.create({
            providers: [{ provide: SI_TOUR_TOKEN, useValue: this.tourToken }],
            parent: this.injector
        });
        // first the highlighter,
        const hlPortal = new ComponentPortal(SiTourHighlightComponent, undefined, componentInjector);
        this.overlayRefHighlight = this.overlay.create({
            positionStrategy: this.overlay.position().global()
        });
        this.overlayRefHighlight.attach(hlPortal);
        // then the dialog
        this.portal = new ComponentPortal(SiTourComponent, undefined, componentInjector);
        this.overlayRef = this.overlay.create({
            scrollStrategy: this.overlay.scrollStrategies.reposition(),
            direction: isRTL() ? 'rtl' : 'ltr'
        });
        this.overlayRef.attach(this.portal);
        // needs a subscriber, otherwise events will be ignored and the .backdrop CSS hack doesn't help
        this.overlayRef.outsidePointerEvents().subscribe();
    }
    handleResizeSubscription(anchorElement) {
        this.resizeSub?.unsubscribe();
        this.resizeSub = undefined;
        if (anchorElement) {
            this.resizeSub = merge(
            // this catches edge cases e.g. with side-panel
            this.resizeObserver.observe(this.document.body, 20, false), this.resizeObserver.observe(anchorElement.nativeElement, 20, false))
                .pipe(throttleTime(20, undefined, { trailing: true }), map(() => undefined), tap(() => {
                if (!this.isElementVisible(anchorElement?.nativeElement)) {
                    // repositions to center if anchor disappears
                    this.centerOverlay();
                }
                else {
                    this.overlayRef?.updatePosition();
                }
            }))
                .subscribe(this.tourToken.sizeChange);
        }
    }
    isElementVisible(element) {
        const rect = element?.getBoundingClientRect();
        return !!rect?.width && !!rect.height;
    }
    getElement(selectorOrElement) {
        if (typeof selectorOrElement === 'function') {
            selectorOrElement = selectorOrElement();
        }
        if (!selectorOrElement) {
            return undefined;
        }
        if (typeof selectorOrElement === 'string') {
            const element = this.document.querySelector(selectorOrElement);
            return element ?? undefined;
        }
        return selectorOrElement;
    }
    async hide() {
        await this.handleBeforeNextPromise();
        this.currentStepIndex = -1;
        this.currentStep = undefined;
        this.active = false;
        this.resizeSub?.unsubscribe();
        this.positionChangeSub?.unsubscribe();
        this.overlayRef?.dispose();
        this.overlayRef = undefined;
        this.overlayRefHighlight?.dispose();
        this.overlayRefHighlight = undefined;
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: SiTourService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
    static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: SiTourService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: SiTourService, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }], ctorParameters: () => [] });

/**
 * Copyright (c) Siemens 2016 - 2026
 * SPDX-License-Identifier: MIT
 */

/**
 * Generated bundle index. Do not edit.
 */

export { SiTourService };
//# sourceMappingURL=siemens-element-ng-tour.mjs.map
