import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, Optional, ViewEncapsulation, Inject, ElementRef, } from '@angular/core';
import { FocusMonitor } from '@angular/cdk/a11y';
import { ENTER, SPACE } from '@angular/cdk/keycodes';
import { merge } from 'rxjs';
import { RSort } from './sort';
import { rSortAnimations } from './sort-animations';
import { getSortHeaderNotContainedWithinSortError } from './sort-errors';
/**
 * Applies sorting behavior (click to change sort) and styles to an element, including an
 * arrow to display the current sort direction.
 *
 * Must be provided with an id and contained within a parent RSort directive.
 *
 * If used on header cells in a CdkTable, it will automatically default its id from its containing
 * column definition.
 */
export class RSortHeader {
    constructor(_changeDetectorRef, 
    // `RSort` is not optionally injected, but just asserted manually w/ better error.
    // tslint:disable-next-line: lightweight-tokens
    _sort, _columnDef, _focusMonitor, _elementRef) {
        this._changeDetectorRef = _changeDetectorRef;
        this._sort = _sort;
        this._columnDef = _columnDef;
        this._focusMonitor = _focusMonitor;
        this._elementRef = _elementRef;
        /**
         * Flag set to true when the indicator should be displayed while the sort is not active. Used to
         * provide an affordance that the header is sortable by showing on focus and hover.
         */
        this._showIndicatorHint = false;
        /**
         * The view transition state of the arrow (translation/ opacity) - indicates its `from` and `to`
         * position through the animation. If animations are currently disabled, the fromState is removed
         * so that there is no animation displayed.
         */
        this._viewState = {};
        /** The direction the arrow should be facing according to the current state. */
        this._arrowDirection = '';
        /**
         * Whether the view state animation should show the transition between the `from` and `to` states.
         */
        this._disableViewStateAnimation = false;
        /** Sets the position of the arrow that displays when sorted. */
        this.arrowPosition = 'after';
        // Note that we use a string token for the `_columnDef`, because the value is provided both by
        // `material/table` and `cdk/table` and we can't have the CDK depending on Rerial,
        // and we want to avoid having the sort header depending on the CDK table because
        // of this single reference.
        if (!_sort) {
            throw getSortHeaderNotContainedWithinSortError();
        }
        this._handleStateChanges();
    }
    /** Overrides the disable clear value of the containing RSort for this RSortable. */
    get disableClear() { return this._disableClear; }
    set disableClear(v) { this._disableClear = coerceBooleanProperty(v); }
    ngOnInit() {
        if (!this.id && this._columnDef) {
            this.id = this._columnDef.name;
        }
        // Initialize the direction of the arrow and set the view state to be immediately that state.
        this._updateArrowDirection();
        this._setAnimationTransitionState({ toState: this._isSorted() ? 'active' : this._arrowDirection });
        this._sort.register(this);
    }
    ngAfterViewInit() {
        // We use the focus monitor because we also want to style
        // things differently based on the focus origin.
        this._focusMonitor.monitor(this._elementRef, true).subscribe(origin => {
            const newState = !!origin;
            if (newState !== this._showIndicatorHint) {
                this._setIndicatorHintVisible(newState);
                this._changeDetectorRef.markForCheck();
            }
        });
    }
    ngOnDestroy() {
        this._focusMonitor.stopMonitoring(this._elementRef);
        this._sort.deregister(this);
        this._rerenderSubscription.unsubscribe();
    }
    /**
     * Sets the "hint" state such that the arrow will be semi-transparently displayed as a hint to the
     * user showing what the active sort will become. If set to false, the arrow will fade away.
     */
    _setIndicatorHintVisible(visible) {
        // No-op if the sort header is disabled - should not make the hint visible.
        if (this._isDisabled() && visible) {
            return;
        }
        this._showIndicatorHint = visible;
        if (!this._isSorted()) {
            this._updateArrowDirection();
            if (this._showIndicatorHint) {
                this._setAnimationTransitionState({ fromState: this._arrowDirection, toState: 'hint' });
            }
            else {
                this._setAnimationTransitionState({ fromState: 'hint', toState: this._arrowDirection });
            }
        }
    }
    /**
     * Sets the animation transition view state for the arrow's position and opacity. If the
     * `disableViewStateAnimation` flag is set to true, the `fromState` will be ignored so that
     * no animation appears.
     */
    _setAnimationTransitionState(viewState) {
        this._viewState = viewState || {};
        // If the animation for arrow position state (opacity/translation) should be disabled,
        // remove the fromState so that it jumps right to the toState.
        if (this._disableViewStateAnimation) {
            this._viewState = { toState: viewState.toState };
        }
    }
    /** Triggers the sort on this sort header and removes the indicator hint. */
    _toggleOnInteraction() {
        this._sort.sort(this);
        // Do not show the animation if the header was already shown in the right position.
        if (this._viewState.toState === 'hint' || this._viewState.toState === 'active') {
            this._disableViewStateAnimation = true;
        }
    }
    _handleClick() {
        if (!this._isDisabled()) {
            this._sort.sort(this);
        }
    }
    _handleKeydown(event) {
        if (!this._isDisabled() && (event.keyCode === SPACE || event.keyCode === ENTER)) {
            event.preventDefault();
            this._toggleOnInteraction();
        }
    }
    /** Whether this RSortHeader is currently sorted in either ascending or descending order. */
    _isSorted() {
        return this._sort.active == this.id &&
            (this._sort.direction === 'asc' || this._sort.direction === 'desc');
    }
    /** Returns the animation state for the arrow direction (indicator and pointers). */
    _getArrowDirectionState() {
        return `${this._isSorted() ? 'active-' : ''}${this._arrowDirection}`;
    }
    /** Returns the arrow position state (opacity, translation). */
    _getArrowViewState() {
        const fromState = this._viewState.fromState;
        return (fromState ? `${fromState}-to-` : '') + this._viewState.toState;
    }
    /**
     * Updates the direction the arrow should be pointing. If it is not sorted, the arrow should be
     * facing the start direction. Otherwise if it is sorted, the arrow should point in the currently
     * active sorted direction. The reason this is updated through a function is because the direction
     * should only be changed at specific times - when deactivated but the hint is displayed and when
     * the sort is active and the direction changes. Otherwise the arrow's direction should linger
     * in cases such as the sort becoming deactivated but we want to animate the arrow away while
     * preserving its direction, even though the next sort direction is actually different and should
     * only be changed once the arrow displays again (hint or activation).
     */
    _updateArrowDirection() {
        this._arrowDirection = this._isSorted() ?
            this._sort.direction :
            (this.start || this._sort.start);
    }
    _isDisabled() {
        return this._sort.disabled || this.disabled;
    }
    /**
     * Gets the aria-sort attribute that should be applied to this sort header. If this header
     * is not sorted, returns null so that the attribute is removed from the host element. Aria spec
     * says that the aria-sort property should only be present on one header at a time, so removing
     * ensures this is true.
     */
    _getAriaSortAttribute() {
        if (!this._isSorted()) {
            return 'none';
        }
        return this._sort.direction == 'asc' ? 'ascending' : 'descending';
    }
    /** Whether the arrow inside the sort header should be rendered. */
    _renderArrow() {
        return !this._isDisabled() || this._isSorted();
    }
    /** Handles changes in the sorting state. */
    _handleStateChanges() {
        this._rerenderSubscription =
            merge(this._sort.sortChange, this._sort._stateChanges).subscribe(() => {
                if (this._isSorted()) {
                    this._updateArrowDirection();
                    // Do not show the animation if the header was already shown in the right position.
                    if (this._viewState.toState === 'hint' || this._viewState.toState === 'active') {
                        this._disableViewStateAnimation = true;
                    }
                    this._setAnimationTransitionState({ fromState: this._arrowDirection, toState: 'active' });
                    this._showIndicatorHint = false;
                }
                // If this header was recently active and now no longer sorted, animate away the arrow.
                if (!this._isSorted() && this._viewState && this._viewState.toState === 'active') {
                    this._disableViewStateAnimation = false;
                    this._setAnimationTransitionState({ fromState: 'active', toState: this._arrowDirection });
                }
                this._changeDetectorRef.markForCheck();
            });
    }
}
RSortHeader.decorators = [
    { type: Component, args: [{
                selector: '[r-sort-header]',
                exportAs: 'rSortHeader',
                template: "<!--\r\n  We set the `tabindex` on an element inside the table header, rather than the header itself,\r\n  because of a bug in NVDA where having a `tabindex` on a `th` breaks keyboard navigation in the\r\n  table (see https://github.com/nvaccess/nvda/issues/7718). This allows for the header to both\r\n  be focusable, and have screen readers read out its `aria-sort` state. We prefer this approach\r\n  over having a button with an `aria-label` inside the header, because the button's `aria-label`\r\n  will be read out as the user is navigating the table's cell (see #13012).\r\n\r\n  The approach is based off of: https://dequeuniversity.com/library/aria/tables/sf-sortable-grid\r\n-->\r\n<div class=\"r-sort-header-container r-focus-indicator\"\r\n     [class.r-sort-header-sorted]=\"_isSorted()\"\r\n     [class.r-sort-header-position-before]=\"arrowPosition == 'before'\"\r\n     [attr.tabindex]=\"_isDisabled() ? null : 0\"\r\n     role=\"button\">\r\n\r\n  <!--\r\n    TODO(crisbeto): this div isn't strictly necessary, but we have to keep it due to a large\r\n    number of screenshot diff failures. It should be removed eventually. Note that the difference\r\n    isn't visible with a shorter header, but once it breaks up into multiple lines, this element\r\n    causes it to be center-aligned, whereas removing it will keep the text to the left.\r\n  -->\r\n  <div class=\"r-sort-header-content\">\r\n    <ng-content></ng-content>\r\n  </div>\r\n\r\n  <!-- Disable anirions while a current anirion is running -->\r\n  <div class=\"r-sort-header-arrow\"\r\n       *ngIf=\"_renderArrow()\"\r\n       [@arrowOpacity]=\"_getArrowViewState()\"\r\n       [@arrowPosition]=\"_getArrowViewState()\"\r\n       [@allowChildren]=\"_getArrowDirectionState()\"\r\n       (@arrowPosition.start)=\"_disableViewStateAnirion = true\"\r\n       (@arrowPosition.done)=\"_disableViewStateAnirion = false\">\r\n    <div class=\"r-sort-header-stem\"></div>\r\n    <div class=\"r-sort-header-indicator\" [@indicator]=\"_getArrowDirectionState()\">\r\n      <div class=\"r-sort-header-pointer-left\" [@leftPointer]=\"_getArrowDirectionState()\"></div>\r\n      <div class=\"r-sort-header-pointer-right\" [@rightPointer]=\"_getArrowDirectionState()\"></div>\r\n      <div class=\"r-sort-header-pointer-middle\"></div>\r\n    </div>\r\n  </div>\r\n</div>\r\n",
                host: {
                    'class': 'r-sort-header',
                    '(click)': '_handleClick()',
                    '(keydown)': '_handleKeydown($event)',
                    '(mouseenter)': '_setIndicatorHintVisible(true)',
                    '(mouseleave)': '_setIndicatorHintVisible(false)',
                    '[attr.aria-sort]': '_getAriaSortAttribute()',
                    '[class.r-sort-header-disabled]': '_isDisabled()',
                },
                encapsulation: ViewEncapsulation.None,
                changeDetection: ChangeDetectionStrategy.OnPush,
                inputs: ['disabled'],
                animations: [
                    rSortAnimations.indicator,
                    rSortAnimations.leftPointer,
                    rSortAnimations.rightPointer,
                    rSortAnimations.arrowOpacity,
                    rSortAnimations.arrowPosition,
                    rSortAnimations.allowChildren,
                ],
                styles: [".r-sort-header-container{display:flex;cursor:pointer;align-items:center;letter-spacing:normal;outline:0}[r-sort-header].cdk-keyboard-focused .r-sort-header-container,[r-sort-header].cdk-program-focused .r-sort-header-container{border-bottom:1px solid}.r-sort-header-disabled .r-sort-header-container{cursor:default}.r-sort-header-content{text-align:center;display:flex;align-items:center}.r-sort-header-position-before{flex-direction:row-reverse}.r-sort-header-arrow{height:12px;width:12px;min-width:12px;position:relative;display:flex;opacity:0}.r-sort-header-arrow,[dir=rtl] .r-sort-header-position-before .r-sort-header-arrow{margin:0 0 0 6px}.r-sort-header-position-before .r-sort-header-arrow,[dir=rtl] .r-sort-header-arrow{margin:0 6px 0 0}.r-sort-header-stem{background:currentColor;height:10px;width:2px;margin:auto;display:flex;align-items:center}.r-sort-header-indicator{width:100%;height:2px;display:flex;align-items:center;position:absolute;top:0;left:0}.r-sort-header-pointer-middle{margin:auto;height:2px;width:2px;background:currentColor;transform:rotate(45deg)}.r-sort-header-pointer-left,.r-sort-header-pointer-right{background:currentColor;width:6px;height:2px;position:absolute;top:0}.r-sort-header-pointer-left{transform-origin:right;left:0}.r-sort-header-pointer-right{transform-origin:left;right:0}"]
            },] }
];
RSortHeader.ctorParameters = () => [
    { type: ChangeDetectorRef },
    { type: RSort, decorators: [{ type: Optional }] },
    { type: undefined, decorators: [{ type: Inject, args: ['R_SORT_HEADER_COLUMN_DEF',] }, { type: Optional }] },
    { type: FocusMonitor },
    { type: ElementRef }
];
RSortHeader.propDecorators = {
    id: [{ type: Input, args: ['mat-sort-header',] }],
    arrowPosition: [{ type: Input }],
    start: [{ type: Input }],
    disableClear: [{ type: Input }]
};
//# sourceMappingURL=sort-header.js.map