// #region Imports
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { on } from '@breadstone/mosaik-elements';
import { fadeIn, fadeOut } from '../../../../Animations/Presets/Fade';
import { DomServiceLocator } from '../../../../Dom/Dom';
import { Animatable } from '../../../Behaviors/Animatable';
import { Attachable } from '../../../Behaviors/Attachable';
import { Closeable } from '../../../Behaviors/Closeable';
import { Disableable } from '../../../Behaviors/Disableable';
import { Openable } from '../../../Behaviors/Openable';
import { Slottable } from '../../../Behaviors/Slottable';
import { TextFormattable } from '../../../Behaviors/TextFormattable';
import { Themeable } from '../../../Behaviors/Themeable';
import { Attribute } from '../../../Decorators/AttributeDecorator';
import { Component } from '../../../Decorators/ComponentDecorator';
import { Watch } from '../../../Decorators/WatchDecorator';
import { Placement } from '../../../Types/Placement';
import { Trigger } from '../../../Types/Trigger';
import { CustomElement } from '../../Abstracts/CustomElement';
import { FloatingElement } from '../Floating/FloatingElement';
import { PortalElement } from '../Portal/PortalElement';
import { ProjectionController } from '../Portal/ProjectionController';
import { TextElement } from '../Text/TextElement';
import { tooltipElementCosmopolitanStyle } from './Themes/TooltipElement.Cosmopolitan';
import { tooltipElementJoyStyle } from './Themes/TooltipElement.Joy';
import { tooltipElementMemphisStyle } from './Themes/TooltipElement.Memphis';
import { tooltipElementTemplate } from './TooltipElementTemplate';
// #endregion
/**
 * Tooltip - A contextual popup that displays helpful information when hovering over or focusing on an element.
 *
 * @description
 * The Tooltip component provides contextual help and additional information for UI elements through a small
 * popup that appears on hover, focus, or click interactions. It supports customizable positioning, delays,
 * animations, and trigger modes. Tooltips automatically calculate optimal placement to avoid viewport edges,
 * include optional arrow indicators, and can be controlled programmatically or through user interactions.
 * This component is essential for providing inline help, displaying full text for truncated content, and
 * offering additional context without cluttering the interface.
 *
 * @name Tooltip
 * @element mosaik-tooltip
 * @category Primitives
 *
 * @slot - The trigger element that activates the tooltip
 * @slot content - The tooltip content (text, elements, or formatted content)
 *
 * @csspart floating - The floating container that positions the tooltip
 * @csspart body - The main tooltip body containing the content
 * @csspart arrow - The optional arrow pointing to the trigger element
 * @csspart text - The text content display element
 *
 * @cssprop {String} --tooltip-background-color - Background color of the tooltip
 * @cssprop {String} --tooltip-border-color - Border color of the tooltip outline
 * @cssprop {String} --tooltip-border-radius - Corner rounding radius
 * @cssprop {String} --tooltip-border-style - Border line style
 * @cssprop {String} --tooltip-border-width - Border thickness
 * @cssprop {String} --tooltip-font-family - Font family for tooltip text
 * @cssprop {String} --tooltip-font-size - Font size for tooltip text
 * @cssprop {String} --tooltip-font-weight - Font weight for tooltip text
 * @cssprop {String} --tooltip-font-line-height - Line height for tooltip text
 * @cssprop {String} --tooltip-font-letter-spacing - Letter spacing for tooltip text
 * @cssprop {String} --tooltip-font-text-decoration - Text decoration style
 * @cssprop {String} --tooltip-font-text-transform - Text transformation style
 * @cssprop {String} --tooltip-foreground-color - Text color
 * @cssprop {String} --tooltip-gap - Spacing between tooltip elements
 * @cssprop {String} --tooltip-max-width - Maximum width of the tooltip
 * @cssprop {String} --tooltip-padding-top - Top padding inside the tooltip
 * @cssprop {String} --tooltip-padding-right - Right padding inside the tooltip
 * @cssprop {String} --tooltip-padding-bottom - Bottom padding inside the tooltip
 * @cssprop {String} --tooltip-padding-left - Left padding inside the tooltip
 *
 * @dependency {TextElement} - Text rendering component for tooltip content
 * @dependency {PortalElement} - Portal component for rendering outside DOM hierarchy
 * @dependency {FloatingElement} - Floating positioning component
 *
 * @fires opened {OpenedEvent} - Fired when the tooltip is opened
 * @fires closed {ClosedEvent} - Fired when the tooltip is closed
 *
 * @example
 * Basic tooltip on hover:
 * ```html
 * <mosaik-tooltip content="Click to save your changes">
 *   <mosaik-button label="Save"></mosaik-button>
 * </mosaik-tooltip>
 * ```
 *
 * @example
 * Tooltip with custom placement:
 * ```html
 * <mosaik-tooltip content="User profile settings" placement="right">
 *   <mosaik-icon data="user-icon"></mosaik-icon>
 * </mosaik-tooltip>
 * ```
 *
 * @example
 * Tooltip with click trigger:
 * ```html
 * <mosaik-tooltip content="Detailed information here" trigger="click" stays-open>
 *   <span>Click for info</span>
 * </mosaik-tooltip>
 * ```
 *
 * @example
 * Tooltip with custom delays:
 * ```html
 * <mosaik-tooltip
 *   content="This appears after 500ms"
 *   show-delay="500"
 *   hide-delay="200">
 *   <mosaik-button label="Hover me"></mosaik-button>
 * </mosaik-tooltip>
 * ```
 *
 * @example
 * Programmatic tooltip control:
 * ```html
 * <mosaik-tooltip id="myTooltip" content="Programmatically controlled" trigger="manual">
 *   <mosaik-button id="trigger" label="Show tooltip"></mosaik-button>
 * </mosaik-tooltip>
 * <script>
 *   const tooltip = document.querySelector('#myTooltip');
 *   const trigger = document.querySelector('#trigger');
 *   trigger.addEventListener('click', () => {
 *     tooltip.open();
 *   });
 * </script>
 * ```
 *
 * @public
 */
let TooltipElement = class TooltipElement extends Themeable(Animatable(Attachable(Disableable(TextFormattable(Openable(Closeable(Slottable(CustomElement)))))))) {
    // #region Fields
    _projectionController;
    _floatingElement;
    _content;
    _placement;
    _horizontalOffset;
    _verticalOffset;
    _isOpen;
    _showDelay;
    _hideDelay;
    _trigger;
    _staysOpen;
    _hoverTimeout;
    _blurSubscription;
    _focusSubscription;
    _clickSubscription;
    _keydownSubscription;
    _mouseoverSubscription;
    _mouseoutSubscription;
    _slottedControl;
    _anchorReady;
    // #endregion
    // #region Ctor
    /**
     * @public
     */
    constructor() {
        super();
        this._floatingElement = null;
        this._content = '';
        this._placement = Placement.Bottom;
        this._horizontalOffset = 8;
        this._verticalOffset = 0;
        this._isOpen = false;
        this._showDelay = 150; // material is 300
        this._hideDelay = 0; // material is 100
        this._trigger = Trigger.Hover;
        this._staysOpen = false;
        this._hoverTimeout = null;
        this._blurSubscription = null;
        this._focusSubscription = null;
        this._clickSubscription = null;
        this._keydownSubscription = null;
        this._mouseoverSubscription = null;
        this._mouseoutSubscription = null;
        this._slottedControl = null;
        this._anchorReady = false;
        this._projectionController = new ProjectionController(this, {
            slotName: 'content',
            targetSelector: '[part="projection"]'
        });
        // Set default animations for FloatingElement
        Object.assign(this, { enter: fadeIn });
        Object.assign(this, { exit: fadeOut });
    }
    // #endregion
    // #region Properties
    /**
     * Returns the `is` property.
     *
     * @public
     * @static
     * @readonly
    */
    static get is() {
        return 'mosaik-tooltip';
    }
    /**
     * Gets whether slotted content is projected into the tooltip.
     *
     * @public
     * @readonly
     */
    get hasSlottedContent() {
        return this._projectionController.elements.length > 0;
    }
    /**
     * Gets the formatted content string.
     *
     * @public
     * @readonly
     */
    get formattedContent() {
        return this.formatter
            ? this.formatter(this._content)
            : this._content;
    }
    /**
     * Gets or sets the `content` property.
     *
     * @public
     * @attr
     */
    get content() {
        return this._content;
    }
    set content(value) {
        if (this._content !== value) {
            this._content = value;
            this.requestUpdate('content');
        }
    }
    /**
     * Gets or sets the `placement` property.
     *
     * @public
     * @attr
     */
    get placement() {
        return this._placement;
    }
    set placement(value) {
        if (this._placement !== value) {
            this._placement = value;
            this.requestUpdate('placement');
        }
    }
    /**
     * Gets or sets the `horizontalOffset` property.
     *
     * @public
     * @attr
     */
    get horizontalOffset() {
        return this._horizontalOffset;
    }
    set horizontalOffset(value) {
        if (this._horizontalOffset !== value) {
            this._horizontalOffset = value;
            this.requestUpdate('horizontalOffset');
        }
    }
    /**
     * Gets or sets the `verticalOffset` property.
     *
     * @public
     * @attr
     */
    get verticalOffset() {
        return this._verticalOffset;
    }
    set verticalOffset(value) {
        if (this._verticalOffset !== value) {
            this._verticalOffset = value;
            this.requestUpdate('verticalOffset');
        }
    }
    /**
     * Gets or sets the `isOpen` property.
     *
     * @public
     * @attr
     */
    get isOpen() {
        return this._isOpen;
    }
    set isOpen(value) {
        if (this._isOpen !== value) {
            this._isOpen = value;
            this.requestUpdate('isOpen');
        }
    }
    /**
     * Gets or sets the `showDelay` property.
     *
     * @public
     * @attr
     */
    get showDelay() {
        return this._showDelay;
    }
    set showDelay(value) {
        if (this._showDelay !== value) {
            this._showDelay = value;
            this.requestUpdate('showDelay');
        }
    }
    /**
     * Gets or sets the `hideDelay` property.
     *
     * @public
     * @attr
     */
    get hideDelay() {
        return this._hideDelay;
    }
    set hideDelay(value) {
        if (this._hideDelay !== value) {
            this._hideDelay = value;
            this.requestUpdate('hideDelay');
        }
    }
    /**
     * Gets or sets the `trigger` property.
     *
     * @public
     * @attr
     */
    get trigger() {
        return this._trigger;
    }
    set trigger(value) {
        if (this._trigger !== value) {
            this._trigger = value;
            this.requestUpdate('trigger');
        }
    }
    /**
     * Gets or sets the `staysOpen` property.
     *
     * @public
     * @attr
     */
    get staysOpen() {
        return this._staysOpen;
    }
    set staysOpen(value) {
        if (this._staysOpen !== value) {
            this._staysOpen = value;
            this.requestUpdate('staysOpen');
        }
    }
    /**
     * Gets the anchor element for the floating popup.
     * This is the slotted control or the explicitly attached control.
     *
     * @public
     * @readonly
     */
    get anchorElement() {
        return this._slottedControl ?? this.control;
    }
    /**
     * Gets whether the floating popup should be active.
     * FloatingElement handles its own enter/exit animations.
     *
     * @public
     * @readonly
     */
    get isFloatingActive() {
        return this.isOpen && this._anchorReady && this.anchorElement !== null;
    }
    // #endregion
    // #region Methods
    /**
     * Opens the tooltip.
     *
     * @public
     * @override
     */
    async open() {
        await super.open()
            .then(() => {
            this.isOpen = true;
        });
    }
    /**
     * Closes the tooltip.
     *
     * @public
     * @override
     */
    async close() {
        if (this._staysOpen) {
            return Promise.resolve(false);
        }
        if (!this._isOpen) {
            return Promise.resolve(true);
        }
        return super.close().then((x) => this.isOpen = x);
    }
    /**
     * @public
     * @override
     */
    attach(control) {
        super.attach(control);
        void this.updateComplete.then(() => {
            this.unbindControlEvents();
            this.bindControlEvents();
            this.updateFloatingAnchor();
        });
    }
    /**
     * @public
     * @override
     */
    connectedCallback() {
        super.connectedCallback();
        // Defer event binding to allow sibling elements (for="..." targets) to mount first.
        // Using requestAnimationFrame ensures the DOM tree is fully constructed.
        void this.updateComplete.then(() => {
            requestAnimationFrame(() => {
                this.bindControlEvents();
                this.updateFloatingAnchor();
            });
        });
    }
    /**
     * @public
     * @override
     */
    disconnectedCallback() {
        super.disconnectedCallback();
        this.unbindControlEvents();
    }
    /**
    * @protected
    * @override
    */
    onApplyTemplate() {
        super.onApplyTemplate();
        // Listen for slot changes to detect the slotted control element
        const defaultSlot = this.shadowRoot?.querySelector('slot:not([name])');
        if (defaultSlot) {
            defaultSlot.addEventListener('slotchange', () => this.onDefaultSlotChange(defaultSlot));
            // Initial check in case content is already slotted
            this.onDefaultSlotChange(defaultSlot);
        }
        const portalElement = this.getTemplatePart('portal');
        // Wait for portal to attach and project its content
        portalElement.addEventListener('portalAttached', () => {
            const projectionElement = portalElement.projection;
            if (projectionElement) {
                // Use #tooltip ID to find the tooltip body (not the floating element's body)
                this._floatingElement = DomServiceLocator.current.findDescendant(projectionElement, '[part="popup"]', { strict: true });
                // Set anchor after floating element is available
                this.updateFloatingAnchor();
            }
        }, { once: true });
    }
    /**
     * @protected
     * @override
     */
    firstUpdated(changedProperties) {
        super.firstUpdated(changedProperties);
        // If the tooltip is visible on init, trigger a reposition
        if (this.isOpen && this._floatingElement && this._anchorReady) {
            this._floatingElement.reposition();
        }
    }
    /**
     * @protected
     */
    onBlur(_event) {
        if (this.hasTrigger(Trigger.Focus)) {
            void this.close();
        }
    }
    /**
     * @protected
     */
    onClick(_event) {
        if (this.hasTrigger(Trigger.Click)) {
            if (this.isOpen) {
                void this.close();
            }
            else {
                void this.open();
            }
        }
    }
    /**
     * @protected
     */
    onFocus(_event) {
        if (this.hasTrigger(Trigger.Focus)) {
            void this.open();
        }
    }
    /**
     * @protected
     */
    onKeyDown(event) {
        // Pressing escape when the target element has focus should dismiss the tooltip
        if (this.isOpen && event.key === 'Escape') {
            event.stopPropagation();
            void this.close();
        }
    }
    /**
     * @protected
     */
    onMouseOver(_event) {
        if (this.hasTrigger(Trigger.Hover)) {
            if (this._hoverTimeout) {
                clearTimeout(this._hoverTimeout);
            }
            this._hoverTimeout = window.setTimeout(() => void this.open(), this._showDelay);
        }
    }
    /**
     * @protected
     */
    onMouseOut(_event) {
        if (this.hasTrigger(Trigger.Hover)) {
            if (this._hoverTimeout) {
                clearTimeout(this._hoverTimeout);
            }
            this._hoverTimeout = window.setTimeout(() => void this.close(), this._hideDelay);
        }
    }
    /**
     * @protected
     */
    onIsOpenPropertyChange() {
        void this.updateComplete.then(() => {
            if (this.isOpen) {
                if (this.disabled) {
                    return;
                }
                this.opened.emit();
            }
            else {
                const args = { cancel: false };
                this.closed.emit(args);
            }
        });
    }
    /**
     * @protected
     */
    onContentPropertyChange(_oldValue, newValue) {
        this._content = this.formatter
            ? this.formatter(newValue)
            : newValue;
        // Trigger reposition after content changes
        void this.updateComplete.then(() => this._floatingElement?.reposition());
    }
    /**
     * @protected
     */
    onDistancePropertyChange() {
        void this.updateComplete.then(() => {
            this._floatingElement?.reposition();
        });
    }
    /**
     * @protected
     */
    onPlacementPropertyChange() {
        void this.updateComplete.then(() => {
            this._floatingElement?.reposition();
        });
    }
    /**
     * @protected
     */
    onSkiddingPropertyChange() {
        void this.updateComplete.then(() => {
            this._floatingElement?.reposition();
        });
    }
    /**
     * @protected
     */
    onDisabledPropertyChange() {
        if (this.disabled && this.isOpen) {
            void this.close();
        }
    }
    /**
     * Handles changes to the `for` attribute to rebind control events.
     *
     * @protected
     */
    onForPropertyChange() {
        void this.updateComplete.then(() => {
            requestAnimationFrame(() => {
                this.unbindControlEvents();
                this.bindControlEvents();
                this.updateFloatingAnchor();
            });
        });
    }
    /**
     * Handles changes to the default slot to detect control elements.
     *
     * @private
     */
    onDefaultSlotChange(slot) {
        const assignedElements = slot.assignedElements({ flatten: true });
        const previousControl = this._slottedControl;
        // Get the first element as the control
        this._slottedControl = assignedElements.length > 0
            ? assignedElements[0]
            : null;
        // Mark anchor as ready only for slotted controls.
        // For 'for' attribute targets, anchor readiness is handled in connectedCallback/onForPropertyChange
        // after the DOM is fully constructed.
        const wasReady = this._anchorReady;
        if (this._slottedControl !== null) {
            this._anchorReady = true;
        }
        // Rebind events if control changed
        if (previousControl !== this._slottedControl) {
            this.unbindControlEvents();
            this.bindControlEvents();
            this.updateFloatingAnchor();
        }
        // Trigger re-render if anchor readiness changed
        if (wasReady !== this._anchorReady) {
            this.requestUpdate('anchorElement');
        }
    }
    /**
     * Updates the floating element's anchor reference by triggering a re-render.
     * The anchor is bound via the template.
     *
     * @private
     */
    updateFloatingAnchor() {
        // Anchor is bound via template (.anchor="${e.anchorElement}"), trigger re-render
        this._anchorReady = this.anchorElement !== null;
        this.requestUpdate('anchorElement');
    }
    /**
     * Binds event listeners to the control element.
     *
     * @private
     */
    bindControlEvents() {
        const control = this.anchorElement;
        if (!control) {
            return;
        }
        this._blurSubscription = on(control, 'blur', (e) => this.onBlur(e), true);
        this._focusSubscription = on(control, 'focus', (e) => this.onFocus(e), true);
        this._clickSubscription = on(control, 'click', (e) => this.onClick(e));
        this._keydownSubscription = on(control, 'keydown', (e) => this.onKeyDown(e));
        this._mouseoverSubscription = on(control, 'mouseover', (e) => this.onMouseOver(e));
        this._mouseoutSubscription = on(control, 'mouseout', (e) => this.onMouseOut(e));
    }
    /**
     * Unbinds event listeners from the control element.
     *
     * @private
     */
    unbindControlEvents() {
        this._blurSubscription?.dispose();
        this._focusSubscription?.dispose();
        this._clickSubscription?.dispose();
        this._keydownSubscription?.dispose();
        this._mouseoverSubscription?.dispose();
        this._mouseoutSubscription?.dispose();
        this._blurSubscription = null;
        this._focusSubscription = null;
        this._clickSubscription = null;
        this._keydownSubscription = null;
        this._mouseoverSubscription = null;
        this._mouseoutSubscription = null;
    }
    /**
     * @private
     */
    hasTrigger(triggerType) {
        const triggers = this.trigger.split(' ');
        return triggers.includes(triggerType);
    }
};
__decorate([
    Attribute({ type: String }),
    __metadata("design:type", String),
    __metadata("design:paramtypes", [String])
], TooltipElement.prototype, "content", null);
__decorate([
    Attribute({ type: Placement }),
    __metadata("design:type", String),
    __metadata("design:paramtypes", [String])
], TooltipElement.prototype, "placement", null);
__decorate([
    Attribute({ type: Number }),
    __metadata("design:type", Number),
    __metadata("design:paramtypes", [Number])
], TooltipElement.prototype, "horizontalOffset", null);
__decorate([
    Attribute({ type: Number }),
    __metadata("design:type", Number),
    __metadata("design:paramtypes", [Number])
], TooltipElement.prototype, "verticalOffset", null);
__decorate([
    Attribute({
        type: Boolean,
        useDefault: true
    }),
    __metadata("design:type", Boolean),
    __metadata("design:paramtypes", [Boolean])
], TooltipElement.prototype, "isOpen", null);
__decorate([
    Attribute({ type: Number }),
    __metadata("design:type", Number),
    __metadata("design:paramtypes", [Number])
], TooltipElement.prototype, "showDelay", null);
__decorate([
    Attribute({ type: Number }),
    __metadata("design:type", Number),
    __metadata("design:paramtypes", [Number])
], TooltipElement.prototype, "hideDelay", null);
__decorate([
    Attribute({ type: Trigger }),
    __metadata("design:type", String),
    __metadata("design:paramtypes", [String])
], TooltipElement.prototype, "trigger", null);
__decorate([
    Attribute({
        type: Boolean,
        useDefault: true
    }),
    __metadata("design:type", Boolean),
    __metadata("design:paramtypes", [Boolean])
], TooltipElement.prototype, "staysOpen", null);
__decorate([
    Watch('isOpen', { waitUntilFirstUpdate: true }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], TooltipElement.prototype, "onIsOpenPropertyChange", null);
__decorate([
    Watch('content'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, String]),
    __metadata("design:returntype", void 0)
], TooltipElement.prototype, "onContentPropertyChange", null);
__decorate([
    Watch('distance'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], TooltipElement.prototype, "onDistancePropertyChange", null);
__decorate([
    Watch('placement'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], TooltipElement.prototype, "onPlacementPropertyChange", null);
__decorate([
    Watch('skidding'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], TooltipElement.prototype, "onSkiddingPropertyChange", null);
__decorate([
    Watch('disabled'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], TooltipElement.prototype, "onDisabledPropertyChange", null);
__decorate([
    Watch('for'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], TooltipElement.prototype, "onForPropertyChange", null);
TooltipElement = __decorate([
    Component({
        selector: 'mosaik-tooltip',
        template: tooltipElementTemplate,
        themes: {
            joy: tooltipElementJoyStyle,
            memphis: tooltipElementMemphisStyle,
            cosmopolitan: tooltipElementCosmopolitanStyle
        },
        imports: [
            TextElement,
            PortalElement,
            FloatingElement
        ]
    }),
    __metadata("design:paramtypes", [])
], TooltipElement);
export { TooltipElement };
//# sourceMappingURL=TooltipElement.js.map