"use strict";
// *****************************************************************************
// Copyright (C) 2022 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************
Object.defineProperty(exports, "__esModule", { value: true });
exports.SelectComponent = exports.SELECT_COMPONENT_CONTAINER = void 0;
const React = require("react");
const ReactDOM = require("react-dom");
const DOMPurify = require("dompurify");
const widget_1 = require("./widget");
const browser_1 = require("../browser");
require("../../../src/browser/style/select-component.css");
exports.SELECT_COMPONENT_CONTAINER = 'select-component-container';
class SelectComponent extends React.Component {
    constructor(props) {
        super(props);
        this.fieldRef = React.createRef();
        this.dropdownRef = React.createRef();
        this.mountedListeners = new Map();
        this.optimalWidth = 0;
        this.optimalHeight = 0;
        let selected = 0;
        if (typeof props.defaultValue === 'number') {
            selected = props.defaultValue;
        }
        else if (typeof props.defaultValue === 'string') {
            selected = Math.max(props.options.findIndex(e => e.value === props.defaultValue), 0);
        }
        this.state = {
            selected,
            original: selected,
            hover: selected
        };
        let list = document.getElementById(exports.SELECT_COMPONENT_CONTAINER);
        if (!list) {
            list = document.createElement('div');
            list.id = exports.SELECT_COMPONENT_CONTAINER;
            document.body.appendChild(list);
        }
        this.dropdownElement = list;
    }
    get options() {
        return this.props.options;
    }
    get value() {
        var _a;
        return (_a = this.props.options[this.state.selected].value) !== null && _a !== void 0 ? _a : this.state.selected;
    }
    set value(value) {
        let index = -1;
        if (typeof value === 'number') {
            index = value;
        }
        else if (typeof value === 'string') {
            index = this.props.options.findIndex(e => e.value === value);
        }
        if (index >= 0) {
            this.setState({
                selected: index,
                original: index,
                hover: index
            });
        }
    }
    getOptimalWidth() {
        const textWidth = (0, browser_1.measureTextWidth)(this.props.options.map(e => e.label || e.value || '' + (e.detail || '')));
        return Math.ceil(textWidth + 16);
    }
    getOptimalHeight(maxWidth) {
        const firstLine = this.props.options.find(e => e.label || e.value || e.detail);
        if (!firstLine) {
            return 0;
        }
        if (maxWidth) {
            maxWidth = Math.ceil(maxWidth) + 10; // Increase width by 10 due to side padding
        }
        const descriptionHeight = (0, browser_1.measureTextHeight)(this.props.options.map(e => e.description || ''), { maxWidth: `${maxWidth}px` }) + 18;
        const singleLineHeight = (0, browser_1.measureTextHeight)(firstLine.label || firstLine.value || firstLine.detail || '') + 6;
        const optimal = descriptionHeight + singleLineHeight * this.props.options.length;
        return optimal + 20; // Just to be safe, add another 20 pixels here
    }
    attachListeners() {
        var _a;
        const hide = (event) => {
            var _a;
            if (!((_a = this.dropdownRef.current) === null || _a === void 0 ? void 0 : _a.contains(event.target))) {
                this.hide();
            }
        };
        this.mountedListeners.set('scroll', hide);
        this.mountedListeners.set('wheel', hide);
        let parent = (_a = this.fieldRef.current) === null || _a === void 0 ? void 0 : _a.parentElement;
        while (parent) {
            // Workaround for perfect scrollbar, since using `overflow: hidden`
            // neither triggers the `scroll`, `wheel` nor `blur` event
            if (parent.classList.contains('ps')) {
                parent.addEventListener('ps-scroll-y', hide);
            }
            parent = parent.parentElement;
        }
        for (const [key, listener] of this.mountedListeners.entries()) {
            window.addEventListener(key, listener);
        }
    }
    componentWillUnmount() {
        var _a;
        if (this.mountedListeners.size > 0) {
            const eventListener = this.mountedListeners.get('scroll');
            let parent = (_a = this.fieldRef.current) === null || _a === void 0 ? void 0 : _a.parentElement;
            while (parent) {
                parent.removeEventListener('ps-scroll-y', eventListener);
                parent = parent.parentElement;
            }
            for (const [key, listener] of this.mountedListeners.entries()) {
                window.removeEventListener(key, listener);
            }
        }
    }
    render() {
        var _a, _b;
        const { options } = this.props;
        let { selected } = this.state;
        if ((_a = options[selected]) === null || _a === void 0 ? void 0 : _a.separator) {
            selected = this.nextNotSeparator('forwards');
        }
        const selectedItemLabel = (_b = options[selected].label) !== null && _b !== void 0 ? _b : options[selected].value;
        return React.createElement(React.Fragment, null,
            React.createElement("div", { key: "select-component", ref: this.fieldRef, tabIndex: 0, className: "theia-select-component", onClick: e => this.handleClickEvent(e), onBlur: () => {
                    var _a, _b;
                    this.hide();
                    (_b = (_a = this.props).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a);
                }, onFocus: () => { var _a, _b; return (_b = (_a = this.props).onFocus) === null || _b === void 0 ? void 0 : _b.call(_a); }, onKeyDown: e => this.handleKeypress(e) },
                React.createElement("div", { key: "label", className: "theia-select-component-label" }, selectedItemLabel),
                React.createElement("div", { key: "icon", className: `theia-select-component-chevron ${(0, widget_1.codicon)('chevron-down')}` })),
            ReactDOM.createPortal(this.renderDropdown(), this.dropdownElement));
    }
    nextNotSeparator(direction) {
        var _a;
        const { options } = this.props;
        const step = direction === 'forwards' ? 1 : -1;
        const length = this.props.options.length;
        let selected = this.state.selected;
        let count = 0;
        do {
            selected = (selected + step) % length;
            if (selected < 0) {
                selected = length - 1;
            }
            count++;
        } while (((_a = options[selected]) === null || _a === void 0 ? void 0 : _a.separator) && count < length);
        return selected;
    }
    handleKeypress(ev) {
        if (!this.fieldRef.current) {
            return;
        }
        if (ev.key === 'ArrowUp') {
            const selected = this.nextNotSeparator('backwards');
            this.setState({
                selected,
                hover: selected
            });
        }
        else if (ev.key === 'ArrowDown') {
            if (this.state.dimensions) {
                const selected = this.nextNotSeparator('forwards');
                this.setState({
                    selected,
                    hover: selected
                });
            }
            else {
                this.toggleVisibility();
                this.setState({
                    selected: 0,
                    hover: 0,
                });
            }
        }
        else if (ev.key === 'Enter') {
            if (!this.state.dimensions) {
                this.toggleVisibility();
            }
            else {
                const selected = this.state.selected;
                this.selectOption(selected, this.props.options[selected]);
            }
        }
        else if (ev.key === 'Escape' || ev.key === 'Tab') {
            this.hide();
        }
        ev.stopPropagation();
        ev.nativeEvent.stopImmediatePropagation();
    }
    handleClickEvent(event) {
        this.toggleVisibility();
        event.stopPropagation();
        event.nativeEvent.stopImmediatePropagation();
    }
    toggleVisibility() {
        if (!this.fieldRef.current) {
            return;
        }
        if (!this.state.dimensions) {
            const rect = this.fieldRef.current.getBoundingClientRect();
            this.setState({ dimensions: rect });
        }
        else {
            this.hide();
        }
    }
    hide(index) {
        const selectedIndex = index === undefined ? this.state.original : index;
        this.setState({
            dimensions: undefined,
            selected: selectedIndex,
            original: selectedIndex,
            hover: selectedIndex
        });
    }
    renderDropdown() {
        if (!this.state.dimensions) {
            return;
        }
        if (this.mountedListeners.size === 0) {
            // Only attach our listeners once we render our dropdown menu
            this.attachListeners();
            // We can now also calculate the optimal width
            this.optimalWidth = this.getOptimalWidth();
            this.optimalHeight = this.getOptimalHeight(Math.max(this.state.dimensions.width, this.optimalWidth));
        }
        const clientRect = document.getElementById('theia-app-shell').getBoundingClientRect();
        const availableTop = this.state.dimensions.top - clientRect.top;
        const availableBottom = clientRect.top + clientRect.height - this.state.dimensions.bottom;
        // prefer rendering to the bottom unless there is not enough space and more content can be shown to the top
        const invert = availableBottom < this.optimalHeight && (availableBottom - this.optimalHeight) < (availableTop - this.optimalHeight);
        const { options } = this.props;
        const { hover } = this.state;
        const description = options[hover].description;
        const markdown = options[hover].markdown;
        const items = options.map((item, i) => this.renderOption(i, item));
        if (description) {
            let descriptionNode;
            const className = 'theia-select-component-description';
            if (markdown) {
                descriptionNode = React.createElement("div", { key: "description", className: className, dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(description) } }); // eslint-disable-line react/no-danger
            }
            else {
                descriptionNode = React.createElement("div", { key: "description", className: className }, description);
            }
            if (invert) {
                items.unshift(descriptionNode);
            }
            else {
                items.push(descriptionNode);
            }
        }
        const calculatedWidth = Math.max(this.state.dimensions.width, this.optimalWidth);
        const maxWidth = clientRect.width - this.state.dimensions.left;
        return React.createElement("div", { key: "dropdown", className: "theia-select-component-dropdown", style: {
                top: invert ? 'none' : this.state.dimensions.bottom,
                bottom: invert ? clientRect.top + clientRect.height - this.state.dimensions.top : 'none',
                left: this.state.dimensions.left,
                width: Math.min(calculatedWidth, maxWidth),
                maxHeight: clientRect.height - (invert ? clientRect.height - this.state.dimensions.bottom : this.state.dimensions.top) - this.state.dimensions.height,
                position: 'absolute'
            }, ref: this.dropdownRef }, items);
    }
    renderOption(index, option) {
        var _a;
        if (option.separator) {
            return React.createElement("div", { key: index, className: "theia-select-component-separator" });
        }
        const selected = this.state.hover;
        return (React.createElement("div", { key: index, className: `theia-select-component-option${index === selected ? ' selected' : ''}`, onMouseOver: () => {
                this.setState({
                    hover: index
                });
            }, onMouseDown: () => {
                this.selectOption(index, option);
            } },
            React.createElement("div", { key: "value", className: "theia-select-component-option-value" }, (_a = option.label) !== null && _a !== void 0 ? _a : option.value),
            option.detail && React.createElement("div", { key: "detail", className: "theia-select-component-option-detail" }, option.detail)));
    }
    selectOption(index, option) {
        var _a, _b;
        (_b = (_a = this.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, option, index);
        this.hide(index);
    }
}
exports.SelectComponent = SelectComponent;
//# sourceMappingURL=select-component.js.map