/*!
 * Custom elements for Nanopore-Digital Web applications
 */
import { Host, } from "@stencil/core";
import { h } from "../../utils/renderer";
import { clamp } from "../../utils/math";
import { debounceEvent } from "../../utils/throttle";
/**
 * Ranges allow the user to select a value within a given range using a slider.
 *
 * @version 1.0.0
 * @status stable
 *
 * @slot start - Content is placed to the left of the range slider in LTR, and to the right in RTL.
 * @slot end - Content is placed to the right of the range slider in LTR, and to the left in RTL.
 */
export class Range {
    noUpdate = false;
    rect;
    hasFocus = false;
    rangeSlider;
    gesture;
    el;
    ratioA = 0;
    ratioB = 0;
    pressedKnob;
    /**
     * How long, in milliseconds, to wait to trigger the
     * `nanoChange` event after each change in the range value.
     */
    debounce = 0;
    debounceChanged() {
        this.nanoChange = debounceEvent(this.nanoChange, this.debounce);
    }
    /**
     * The name of the control, which is submitted with the form data.
     */
    name = '';
    /**
     * If `true`, the user must fill in a value before submitting a form.
     * By default, setting this will also add an asterisk to the label.
     */
    required = false;
    /**
     * Visually hide the label - but make it accessible.
     */
    hideLabel = false;
    _label = '';
    /**
     * String to place within a label element. Alternatively you may use a label slot.
     */
    get label() {
        return this.required && !this._label.endsWith('*')
            ? this._label + ' *'
            : this._label;
    }
    set label(value) {
        this._label = value;
    }
    /**
     * Show two knobs.
     */
    dualKnobs = false;
    /**
     * Minimum integer value of the range.
     */
    min = 0;
    minChanged() {
        if (!this.noUpdate) {
            this.updateRatio();
        }
    }
    /**
     * Maximum integer value of the range.
     */
    max = 100;
    maxChanged() {
        if (!this.noUpdate) {
            this.updateRatio();
        }
    }
    /**
     * If `true`, a tooltip with integer value is shown when the knob
     * is pressed.
     */
    tooltip = true;
    /**
     * If `true`, the knob snaps to tick marks evenly spaced based
     * on the `step` property value.
     */
    snaps = false;
    /**
     * Specifies the value granularity.
     */
    step = 1;
    /**
     * If `true`, tick marks are displayed based on the step value.
     * Only applies when `snaps` is `true`.
     */
    ticks = false;
    /**
     * If `true`, the user cannot interact with the range.
     */
    disabled = false;
    disabledChanged() {
        if (this.gesture) {
            this.gesture.enable(!this.disabled);
        }
        this.emitStyle();
    }
    /**
     * the value of the range.
     */
    value = 0;
    valueChanged(value) {
        if (!this.noUpdate) {
            this.updateRatio();
        }
        value = this.ensureValueInBounds(value);
        this.nanoChange.emit({ value });
    }
    clampBounds = (value) => {
        return clamp(this.min, value, this.max);
    };
    ensureValueInBounds = (value) => {
        if (this.dualKnobs) {
            return {
                lower: this.clampBounds(value.lower),
                upper: this.clampBounds(value.upper),
            };
        }
        else {
            return this.clampBounds(value);
        }
    };
    /**
     * Emitted when the value property has changed.
     */
    nanoChange;
    /**
     * Emitted when the styles change.
     * @internal
     */
    nanoStyle;
    /**
     * Emitted when the range has focus.
     */
    nanoFocus;
    /**
     * Emitted when the range loses focus.
     */
    nanoBlur;
    connectedCallback() {
        this.updateRatio();
        this.debounceChanged();
        this.disabledChanged();
    }
    disconnectedCallback() {
        if (this.gesture) {
            this.gesture.destroy();
            this.gesture = undefined;
        }
    }
    async componentDidLoad() {
        const rangeSlider = this.rangeSlider;
        if (rangeSlider) {
            this.gesture = (await import('../../utils/gesture/index')).createGesture({
                el: rangeSlider,
                gestureName: 'range',
                gesturePriority: 100,
                threshold: 0,
                onStart: (ev) => this.onStart(ev),
                onMove: (ev) => this.onMove(ev),
                onEnd: (ev) => this.onEnd(ev),
            });
            this.gesture.enable(!this.disabled);
        }
    }
    handleKeyboard = (knob, isIncrease) => {
        let step = this.step;
        step = step > 0 ? step : 1;
        step = step / (this.max - this.min);
        if (!isIncrease) {
            step *= -1;
        }
        if (knob === 'A') {
            this.ratioA = clamp(0, this.ratioA + step, 1);
        }
        else {
            this.ratioB = clamp(0, this.ratioB + step, 1);
        }
        this.updateValue();
    };
    getValue() {
        const value = this.value || 0;
        if (this.dualKnobs) {
            if (typeof value === 'object') {
                return value;
            }
            return {
                lower: 0,
                upper: value,
            };
        }
        else {
            if (typeof value === 'object') {
                return value.upper;
            }
            return value;
        }
    }
    emitStyle() {
        this.nanoStyle.emit({
            interactive: true,
            'interactive-disabled': this.disabled,
        });
    }
    onStart(detail) {
        const rect = (this.rect = this.rangeSlider.getBoundingClientRect());
        const currentX = detail.currentX;
        // figure out which knob they started closer to
        let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
        if (document.dir === 'rtl') {
            ratio = 1 - ratio;
        }
        this.pressedKnob =
            !this.dualKnobs ||
                Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio)
                ? 'A'
                : 'B';
        this.setFocus(this.pressedKnob);
        // update the active knob's position
        this.update(currentX);
    }
    onMove(detail) {
        this.update(detail.currentX);
    }
    onEnd(detail) {
        this.update(detail.currentX);
        this.pressedKnob = undefined;
    }
    update(currentX) {
        // figure out where the pointer is currently at
        // update the knob being interacted with
        const rect = this.rect;
        let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
        if (document.dir === 'rtl') {
            ratio = 1 - ratio;
        }
        if (this.snaps) {
            // snaps the ratio to the current value
            ratio = valueToRatio(ratioToValue(ratio, this.min, this.max, this.step), this.min, this.max);
        }
        // update which knob is pressed
        if (this.pressedKnob === 'A') {
            this.ratioA = ratio;
        }
        else {
            this.ratioB = ratio;
        }
        // Update input value
        this.updateValue();
    }
    get valA() {
        return ratioToValue(this.ratioA, this.min, this.max, this.step);
    }
    get valB() {
        return ratioToValue(this.ratioB, this.min, this.max, this.step);
    }
    get ratioLower() {
        if (this.dualKnobs) {
            return Math.min(this.ratioA, this.ratioB);
        }
        return 0;
    }
    get ratioUpper() {
        if (this.dualKnobs) {
            return Math.max(this.ratioA, this.ratioB);
        }
        return this.ratioA;
    }
    updateRatio() {
        const value = this.getValue();
        const { min, max } = this;
        if (this.dualKnobs) {
            this.ratioA = valueToRatio(value.lower, min, max);
            this.ratioB = valueToRatio(value.upper, min, max);
        }
        else {
            this.ratioA = valueToRatio(value, min, max);
        }
    }
    updateValue() {
        this.noUpdate = true;
        const { valA, valB } = this;
        this.value = !this.dualKnobs
            ? valA
            : {
                lower: Math.min(valA, valB),
                upper: Math.max(valA, valB),
            };
        this.noUpdate = false;
    }
    setFocus(knob) {
        const knobEl = this.el.querySelector(knob === 'A' ? '.range-knob-a' : '.range-knob-b');
        if (knobEl) {
            knobEl.focus();
        }
    }
    onBlur = () => {
        if (this.hasFocus) {
            this.hasFocus = false;
            this.nanoBlur.emit();
            this.emitStyle();
        }
    };
    onFocus = () => {
        if (!this.hasFocus) {
            this.hasFocus = true;
            this.nanoFocus.emit();
            this.emitStyle();
        }
    };
    render() {
        const { min, max, step, handleKeyboard, pressedKnob, disabled, tooltip, ratioLower, ratioUpper, } = this;
        const barStart = `${ratioLower * 100}%`;
        const barEnd = `${100 - ratioUpper * 100}%`;
        const doc = document;
        const isRTL = doc.dir === 'rtl';
        const start = isRTL ? 'right' : 'left';
        const end = isRTL ? 'left' : 'right';
        const tickStyle = (tick) => {
            return {
                [start]: tick[start],
            };
        };
        const barStyle = {
            [start]: barStart,
            [end]: barEnd,
        };
        const ticks = [];
        if (this.snaps && this.ticks) {
            for (let value = min; value <= max; value += step) {
                const ratio = valueToRatio(value, min, max);
                const tick = {
                    ratio,
                    active: ratio >= ratioLower && ratio <= ratioUpper,
                };
                tick[start] = `${ratio * 100}%`;
                ticks.push(tick);
            }
        }
        return (h(Host, { onFocusin: this.onFocus, onFocusout: this.onBlur, class: {
                'range-disabled': disabled,
                'range-pressed': pressedKnob !== undefined,
                'range-has-pin': tooltip,
                'range-snaps': this.snaps,
                'nano-range': true,
            } }, h("label", { class: `range-label${this.hideLabel ? ' visually-hidden' : ''}`, part: "label", htmlFor: this.name }, h("slot", { name: "label" }, this.label)), h("input", { class: "range-input visually-hidden", name: this.name, id: this.name, disabled: this.disabled, required: this.required, value: JSON.stringify(this.getValue()), onFocus: () => {
                this.setFocus(this.pressedKnob || 'A');
            } }), h("div", { class: "range-wrap" }, h("slot", { name: "start" }), h("div", { class: "range-slider", ref: (rangeEl) => (this.rangeSlider = rangeEl) }, ticks.map((tick) => (h("span", { style: tickStyle(tick), role: "presentation", class: {
                'range-tick': true,
                'range-tick-active': tick.active,
            }, part: tick.active ? 'tick-active' : 'tick' }))), h("div", { class: "range-bar", role: "presentation", part: "bar" }), h("div", { class: "range-bar range-bar-active", role: "presentation", style: barStyle, part: "bar-active" }), renderKnob(isRTL, {
            knob: 'A',
            pressed: pressedKnob === 'A',
            value: this.valA,
            ratio: this.ratioA,
            tooltip,
            disabled,
            handleKeyboard,
            min,
            max,
        }), this.dualKnobs &&
            renderKnob(isRTL, {
                knob: 'B',
                pressed: pressedKnob === 'B',
                value: this.valB,
                ratio: this.ratioB,
                tooltip,
                disabled,
                handleKeyboard,
                min,
                max,
            })), h("slot", { name: "end" }))));
    }
    static get is() { return "nano-range"; }
    static get encapsulation() { return "scoped"; }
    static get originalStyleUrls() {
        return {
            "$": ["range.scss"]
        };
    }
    static get styleUrls() {
        return {
            "$": ["range.css"]
        };
    }
    static get properties() {
        return {
            "debounce": {
                "type": "number",
                "attribute": "debounce",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "How long, in milliseconds, to wait to trigger the\n`nanoChange` event after each change in the range value."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "0"
            },
            "name": {
                "type": "string",
                "attribute": "name",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "The name of the control, which is submitted with the form data."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "''"
            },
            "required": {
                "type": "boolean",
                "attribute": "required",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If `true`, the user must fill in a value before submitting a form.\nBy default, setting this will also add an asterisk to the label."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            },
            "hideLabel": {
                "type": "boolean",
                "attribute": "hide-label",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Visually hide the label - but make it accessible."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            },
            "label": {
                "type": "string",
                "attribute": "label",
                "mutable": true,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "String to place within a label element. Alternatively you may use a label slot."
                },
                "getter": true,
                "setter": true,
                "reflect": false
            },
            "dualKnobs": {
                "type": "boolean",
                "attribute": "dual-knobs",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Show two knobs."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            },
            "min": {
                "type": "number",
                "attribute": "min",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Minimum integer value of the range."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "0"
            },
            "max": {
                "type": "number",
                "attribute": "max",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Maximum integer value of the range."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "100"
            },
            "tooltip": {
                "type": "boolean",
                "attribute": "tooltip",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If `true`, a tooltip with integer value is shown when the knob\nis pressed."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "true"
            },
            "snaps": {
                "type": "boolean",
                "attribute": "snaps",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If `true`, the knob snaps to tick marks evenly spaced based\non the `step` property value."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            },
            "step": {
                "type": "number",
                "attribute": "step",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Specifies the value granularity."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "1"
            },
            "ticks": {
                "type": "boolean",
                "attribute": "ticks",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If `true`, tick marks are displayed based on the step value.\nOnly applies when `snaps` is `true`."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            },
            "disabled": {
                "type": "boolean",
                "attribute": "disabled",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If `true`, the user cannot interact with the range."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            },
            "value": {
                "type": "number",
                "attribute": "value",
                "mutable": true,
                "complexType": {
                    "original": "RangeValue",
                    "resolved": "number | { lower: number; upper: number; }",
                    "references": {
                        "RangeValue": {
                            "location": "import",
                            "path": "./range-interface",
                            "id": "src/components/range/range-interface.d.ts::RangeValue"
                        }
                    }
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "the value of the range."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "0"
            }
        };
    }
    static get states() {
        return {
            "ratioA": {},
            "ratioB": {},
            "pressedKnob": {}
        };
    }
    static get events() {
        return [{
                "method": "nanoChange",
                "name": "nanoChange",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the value property has changed."
                },
                "complexType": {
                    "original": "RangeChangeEventDetail",
                    "resolved": "RangeChangeEventDetail",
                    "references": {
                        "RangeChangeEventDetail": {
                            "location": "import",
                            "path": "./range-interface",
                            "id": "src/components/range/range-interface.d.ts::RangeChangeEventDetail"
                        }
                    }
                }
            }, {
                "method": "nanoStyle",
                "name": "nanoStyle",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [{
                            "name": "internal",
                            "text": undefined
                        }],
                    "text": "Emitted when the styles change."
                },
                "complexType": {
                    "original": "StyleEventDetail",
                    "resolved": "StyleEventDetail",
                    "references": {
                        "StyleEventDetail": {
                            "location": "import",
                            "path": "../../types/shared",
                            "id": "src/types/shared.ts::StyleEventDetail"
                        }
                    }
                }
            }, {
                "method": "nanoFocus",
                "name": "nanoFocus",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the range has focus."
                },
                "complexType": {
                    "original": "void",
                    "resolved": "void",
                    "references": {}
                }
            }, {
                "method": "nanoBlur",
                "name": "nanoBlur",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the range loses focus."
                },
                "complexType": {
                    "original": "void",
                    "resolved": "void",
                    "references": {}
                }
            }];
    }
    static get elementRef() { return "el"; }
    static get watchers() {
        return [{
                "propName": "debounce",
                "methodName": "debounceChanged"
            }, {
                "propName": "min",
                "methodName": "minChanged"
            }, {
                "propName": "max",
                "methodName": "maxChanged"
            }, {
                "propName": "disabled",
                "methodName": "disabledChanged"
            }, {
                "propName": "value",
                "methodName": "valueChanged"
            }];
    }
}
const renderKnob = (isRTL, { knob, value, ratio, min, max, disabled, pressed, tooltip, handleKeyboard, }) => {
    const start = isRTL ? 'right' : 'left';
    const knobStyle = () => {
        const style = {};
        style[start] = `${ratio * 100}%`;
        return style;
    };
    return (h("div", { onKeyDown: (ev) => {
            const key = ev.key;
            if (key === 'ArrowLeft' || key === 'ArrowDown') {
                handleKeyboard(knob, false);
                ev.preventDefault();
                ev.stopPropagation();
            }
            else if (key === 'ArrowRight' || key === 'ArrowUp') {
                handleKeyboard(knob, true);
                ev.preventDefault();
                ev.stopPropagation();
            }
        }, class: {
            'range-knob-handle': true,
            'range-knob-a': knob === 'A',
            'range-knob-b': knob === 'B',
            'range-knob-pressed': pressed,
            'range-knob-min': value === min,
            'range-knob-max': value === max,
        }, style: knobStyle(), role: "slider", tabindex: disabled ? -1 : 0, "aria-valuemin": min, "aria-valuemax": max, "aria-disabled": disabled ? 'true' : null, "aria-valuenow": value }, tooltip && (h("div", { class: "range-pin", role: "presentation", part: "pin" }, Math.round(value))), h("div", { class: "range-knob", role: "presentation", part: "knob" })));
};
const ratioToValue = (ratio, min, max, step) => {
    let value = (max - min) * ratio;
    if (step > 0) {
        value = Math.round(value / step) * step + min;
    }
    return clamp(min, value, max);
};
const valueToRatio = (value, min, max) => {
    return clamp(0, (value - min) / (max - min), 1);
};
