"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SearchableDrowndown = SearchableDrowndown;
const React = __importStar(require("react"));
const ReactContext_1 = __importDefault(require("../../ReactContext"));
const utils_1 = require("../../utils");
const react_1 = require("react");
const react_dom_1 = require("react-dom");
const react_spinner_1 = __importDefault(require("react-spinner"));
const useRangeIncrementor = (length, defaultIdx) => {
    const [idx, _setIdx] = (0, react_1.useState)(defaultIdx);
    const setIdx = (0, react_1.useCallback)((idx) => {
        let nextIdx = idx;
        if (idx === undefined || idx < 0 || length === 0) {
            nextIdx = undefined;
        }
        else if (idx >= length) {
            nextIdx = length - 1;
        }
        _setIdx(nextIdx);
    }, [_setIdx, length]);
    const increment = (0, react_1.useCallback)(() => setIdx((idx || 0) - 1), [idx, setIdx]);
    const decrement = (0, react_1.useCallback)(() => setIdx(idx === undefined ? 0 : idx + 1), [idx, setIdx]);
    return [idx === undefined ? idx : Math.min(idx, length - 1), increment, decrement, _setIdx];
};
function removeByIndex(array, index) {
    return [...array.slice(0, index), ...array.slice(index + 1)];
}
function getEnumOptions(enumOptions, uiSchema, includeEmpty = true) {
    const enums = ((0, utils_1.getUiOptions)(uiSchema).enumOptions || enumOptions);
    const emptyIdx = enums.findIndex(e => e.value === "");
    if (!includeEmpty) {
        return emptyIdx !== -1
            ? removeByIndex(enums, emptyIdx)
            : enums;
    }
    else {
        return emptyIdx !== -1
            ? enums.map(e => e.value === "" ? { value: undefined, label: "" } : e)
            : [{ value: undefined, label: "" }, ...enums];
    }
}
const SelectWidget = (props) => props.schema.type === "array"
    ? React.createElement(SearchableMultiDrowndown, Object.assign({}, props))
    : React.createElement(SearchableDrowndown, Object.assign({}, props));
exports.default = SelectWidget;
function SearchableDrowndown(props) {
    const { id, disabled, readonly, value, uiSchema, options, onChange, includeEmpty = true } = props;
    const { theme } = (0, react_1.useContext)(ReactContext_1.default);
    const { FormControl } = theme;
    const containerRef = (0, react_1.useRef)(null);
    const inputRef = (0, react_1.useRef)(null);
    const dropdownRef = (0, react_1.useRef)(null);
    const allEnumOptions = (0, react_1.useMemo)(() => getEnumOptions(options.enumOptions, uiSchema, includeEmpty), [options.enumOptions, uiSchema, includeEmpty]);
    const enumOptions = (0, react_1.useMemo)(() => {
        if (options.whitelist) {
            return allEnumOptions.filter(e => e.value === undefined || options.whitelist.includes(e.value));
        }
        return allEnumOptions;
    }, [allEnumOptions, options.whitelist]);
    const getLabelFromValue = (0, react_1.useCallback)((value) => value !== undefined && value !== "" && value !== null
        ? allEnumOptions.find(item => item.value === value).label
        : "", [allEnumOptions]);
    const [filterTerm, setFilterTerm] = (0, react_1.useState)(undefined);
    const [isOpen, show, hide] = (0, utils_1.useBooleanSetter)(false);
    const filteredEnums = (0, react_1.useMemo)(() => {
        return filterTerm !== undefined && filterTerm !== ""
            ? enumOptions.filter(({ label }) => label.toLowerCase().match(filterTerm.toLowerCase()))
            : enumOptions;
    }, [filterTerm, enumOptions]);
    const [activeIdx, activeIdxUp, activeIdxDown, setActiveIdx] = useRangeIncrementor((filteredEnums || []).length, getDefaultActiveIdx(filteredEnums, value));
    (0, react_1.useEffect)(() => {
        setFilterTerm(undefined);
        setActiveIdx(Math.max(enumOptions.findIndex(enu => enu.value === value), 0));
    }, [enumOptions, setActiveIdx, value]);
    const inputValue = filterTerm !== null && filterTerm !== void 0 ? filterTerm : getLabelFromValue(value);
    const showAndSelectText = (0, react_1.useCallback)(() => {
        var _a;
        show();
        (_a = (0, react_dom_1.findDOMNode)(inputRef.current)) === null || _a === void 0 ? void 0 : _a.setSelectionRange(0, inputValue.length);
    }, [show, inputValue.length]);
    const onInputChange = (0, react_1.useCallback)((e) => {
        const { value } = e.target;
        setFilterTerm(value);
        setActiveIdx(0);
    }, [setActiveIdx]);
    const onItemSelected = (0, react_1.useCallback)((item) => {
        if (item.value === value) {
            return;
        }
        onChange(item.value);
        hide();
    }, [hide, onChange, value]);
    const onBlur = (0, react_1.useCallback)((e) => {
        // Fixes the issue that when user tries to click an enum item, `setOpen(false)`
        // hides the enum list, so the elem list item is hidden before the click, thus never
        // sending a click event.
        if (e.relatedTarget && (0, utils_1.isDescendant)(containerRef.current, e.relatedTarget)) {
            return;
        }
        if (activeIdx !== undefined && filteredEnums[activeIdx]) {
            onItemSelected(filteredEnums[activeIdx]);
        }
        else {
            setFilterTerm(undefined);
        }
        hide();
    }, [activeIdx, filteredEnums, hide, onItemSelected]);
    const onKeyDown = (0, react_1.useCallback)((e) => {
        switch (e.key) {
            case "ArrowDown":
                if (!isOpen) {
                    showAndSelectText();
                }
                else {
                    activeIdxDown();
                }
                e.preventDefault();
                break;
            case "ArrowUp":
                if (!isOpen) {
                    showAndSelectText();
                }
                else {
                    activeIdxUp();
                }
                e.preventDefault();
                break;
            case "Enter":
                if (activeIdx !== undefined && filteredEnums) {
                    onItemSelected(filteredEnums[activeIdx]);
                }
                e.preventDefault();
                break;
            case "Escape":
                setFilterTerm(undefined);
                setActiveIdx(getDefaultActiveIdx(filteredEnums, value));
                e.preventDefault();
                break;
        }
    }, [activeIdx, activeIdxDown, activeIdxUp, filteredEnums, isOpen, onItemSelected, setActiveIdx, showAndSelectText, value]);
    return (React.createElement("div", { onBlur: onBlur, onKeyDown: onKeyDown, ref: containerRef, style: { position: "relative" }, className: "laji-form-dropdown-container" },
        React.createElement(FormControl, { disabled: disabled || readonly, id: id, onClick: showAndSelectText, onFocus: showAndSelectText, value: inputValue, placeholder: options.placeholder, onChange: onInputChange, autoComplete: "off", ref: inputRef }),
        React.createElement(Caret, null),
        React.createElement("div", { className: `laji-form-dropdown laji-form-dropdown-${isOpen ? "open" : "closed"}`, style: { position: "absolute" }, tabIndex: -1, ref: dropdownRef }, filteredEnums.map((oneOf, idx) => {
            var _a;
            return (React.createElement(ListItem, { key: (_a = oneOf.value) !== null && _a !== void 0 ? _a : "", onSelected: onItemSelected, active: idx === activeIdx }, oneOf));
        }))));
}
function SearchableMultiDrowndown(props) {
    const { id, disabled, readonly, value, uiSchema, options, onChange, getEnumOptionsAsync, formContext } = props;
    const [enumOptions, setEnumOptions] = (0, react_1.useState)(getEnumOptionsAsync
        ? undefined
        : getEnumOptions(options.enumOptions, uiSchema, false));
    const [filterTerm, setFilterTerm] = (0, react_1.useState)();
    const [loading, setLoading] = (0, react_1.useState)(undefined);
    const [isOpen, show, hide] = (0, utils_1.useBooleanSetter)(false);
    const inputValue = filterTerm !== null && filterTerm !== void 0 ? filterTerm : "";
    const containerRef = React.useRef(null);
    const inputRef = React.useRef(null);
    const filteredEnums = React.useMemo(() => {
        if (!enumOptions) {
            return [];
        }
        const notAlreadySelected = (value === null || value === void 0 ? void 0 : value.length)
            ? enumOptions.filter(({ value: enumValue }) => !value.includes(enumValue))
            : enumOptions;
        return filterTerm !== undefined && filterTerm !== ""
            ? notAlreadySelected.filter(({ label, value: enumValue }) => (value || []).includes(enumValue)
                || label.toLowerCase().match(filterTerm.toLowerCase()))
            : notAlreadySelected;
    }, [filterTerm, enumOptions, value]);
    const [activeIdx, activeIdxUp, activeIdxDown, setActiveIdx] = useRangeIncrementor((filteredEnums || []).length, undefined);
    const onInputChange = (0, react_1.useCallback)((e) => {
        const { value } = e.target;
        setFilterTerm(value);
        setActiveIdx(undefined);
    }, [setActiveIdx]);
    const onItemSelected = (0, react_1.useCallback)((item) => {
        var _a;
        onChange([...(value || []), item.value]);
        setFilterTerm(undefined);
        (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
        setActiveIdx(undefined);
    }, [onChange, setActiveIdx, value]);
    const onItemSelectedByBlur = (0, react_1.useCallback)((item) => {
        onChange([...(value || []), item.value]);
        setFilterTerm(undefined);
        setActiveIdx(undefined);
    }, [onChange, setActiveIdx, value]);
    const [isFocused, setFocused, setBlurred] = (0, utils_1.useBooleanSetter)(false);
    const loadEnums = (0, react_1.useCallback)(() => {
        const asyncOp = () => __awaiter(this, void 0, void 0, function* () {
            try {
                const enums = yield getEnumOptionsAsync();
                setEnumOptions(enums);
            }
            finally {
                setLoading(false);
            }
        });
        setLoading(true);
        void asyncOp();
    }, [getEnumOptionsAsync]);
    const prevEnumOptions = (0, utils_1.usePrevious)(enumOptions);
    // If the enums are async loaded, this effect takes care of opening the
    // dropdown once the enums are loaded, if the input if still focused.
    (0, react_1.useEffect)(() => {
        if (!getEnumOptionsAsync) {
            return;
        }
        if (isFocused && !(prevEnumOptions === null || prevEnumOptions === void 0 ? void 0 : prevEnumOptions.length) && (enumOptions === null || enumOptions === void 0 ? void 0 : enumOptions.length)) {
            show();
        }
    }, [prevEnumOptions, enumOptions, isFocused, show, getEnumOptionsAsync]);
    // If there is a pre-existing value (or a value is updated from parent after component is rendered) and we are async,
    // load the enums.
    (0, react_1.useEffect)(() => {
        if ((value === null || value === void 0 ? void 0 : value.length) && getEnumOptionsAsync && !enumOptions && !loading) {
            loadEnums();
        }
    }, [enumOptions, getEnumOptionsAsync, loadEnums, loading, value === null || value === void 0 ? void 0 : value.length]);
    const onFocus = (0, react_1.useCallback)(() => {
        setFocused();
        if (!(enumOptions === null || enumOptions === void 0 ? void 0 : enumOptions.length) && getEnumOptionsAsync) {
            void loadEnums();
        }
        else {
            show();
        }
    }, [enumOptions === null || enumOptions === void 0 ? void 0 : enumOptions.length, getEnumOptionsAsync, loadEnums, setFocused, show]);
    const onBlur = (0, react_1.useCallback)((e) => {
        // Fixes the problem when user tries to click an enum item, `setOpen(false)`
        // hides the enum list, so the elem list item is hidden before the click, thus never
        // sending a click event.
        if (e.relatedTarget && (0, utils_1.isDescendant)(containerRef.current, e.relatedTarget)) {
            return;
        }
        setBlurred();
        hide();
        if (activeIdx !== undefined && filteredEnums[activeIdx]) {
            onItemSelectedByBlur(filteredEnums[activeIdx]);
        }
        else {
            setFilterTerm(undefined);
        }
    }, [activeIdx, filteredEnums, hide, onItemSelectedByBlur, setBlurred]);
    const onKeyDown = (0, react_1.useCallback)((e) => {
        switch (e.key) {
            case "ArrowDown":
                activeIdxDown();
                e.preventDefault();
                break;
            case "ArrowUp":
                activeIdxUp();
                e.preventDefault();
                break;
            case "Enter":
                activeIdx !== undefined && filteredEnums && onItemSelected(filteredEnums[activeIdx]);
                e.preventDefault();
                break;
            case "Escape":
                if (activeIdx !== undefined && filteredEnums) {
                    onItemSelected(filteredEnums[activeIdx]);
                }
                e.preventDefault();
                break;
            case "Backspace":
                if (inputValue === "" && (value === null || value === void 0 ? void 0 : value.length)) {
                    onChange(value.slice(0, -1));
                    e.preventDefault();
                }
                break;
        }
    }, [activeIdx, activeIdxDown, activeIdxUp, filteredEnums, inputValue, onChange, onItemSelected, value]);
    const onDelete = (0, react_1.useCallback)((enu) => {
        var _a;
        const filtered = value.filter(v => v !== enu.value);
        onChange(value.length === 0 ? undefined : filtered);
        if (isOpen) {
            (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
        }
    }, [isOpen, onChange, value]);
    const redirectFocusToInput = (0, react_1.useCallback)((e) => {
        var _a;
        // Only the input wrapper should redirect focus, otherwise the existing label deletion buttons will also redirect, breaking shift + tab navigation on the input.
        if (!e.target.classList.contains("laji-form-multiselect-input-wrapper")) {
            return;
        }
        (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
    }, [inputRef]);
    const wrapperClassNames = (0, utils_1.classNames)("laji-form-multiselect-input-wrapper", isOpen && "laji-form-multiselect-input-wrapper-focus", isOpen && "input-highlight", (readonly || disabled) && "laji-form-multiselect-input-wrapper-readonly");
    const selectedListChildren = value && enumOptions && value.map(v => enumOptions.find(({ value: _value }) => v === _value) || ({ value: v, label: "" + v }))
        .map((enu, idx) => {
        var _a;
        const childId = id ? `${id}_${idx}` : undefined;
        const childIdPointer = childId ? (0, utils_1.idSchemaIdToJSONPointer)(childId) : undefined;
        const classNames = childIdPointer ? (_a = formContext.uiSchemaContext.additionalClassNames) === null || _a === void 0 ? void 0 : _a[childIdPointer] : undefined;
        return React.createElement(SelectedMultiValue, { id: childId, className: classNames, key: enu.value, onDelete: onDelete, readonly: readonly || disabled }, enu);
    });
    return (React.createElement("div", { onBlur: onBlur, onKeyDown: onKeyDown, ref: containerRef, className: "laji-form-multiselect", style: { position: "relative" } },
        React.createElement("div", { className: wrapperClassNames, tabIndex: -1, onFocus: redirectFocusToInput, style: { cursor: "text" } },
            React.createElement("ul", { style: { listStyle: "none", display: "inline-block" } }, selectedListChildren),
            React.createElement("input", { disabled: disabled || readonly, id: id, onFocus: onFocus, value: inputValue, onChange: onInputChange, autoComplete: "off", ref: inputRef }),
            loading && React.createElement(react_spinner_1.default, null)),
        React.createElement(Caret, null),
        React.createElement("div", { className: `laji-form-dropdown laji-form-dropdown-${isOpen ? "open" : "closed"}`, style: { position: "absolute", zIndex: 99999 }, tabIndex: -1 }, filteredEnums.map((oneOf, idx) => {
            var _a;
            return (React.createElement(ListItem, { key: (_a = oneOf.value) !== null && _a !== void 0 ? _a : "", onSelected: onItemSelected, active: idx === activeIdx }, oneOf));
        }))));
}
function SelectedMultiValue({ id, className, children: enu, onDelete, readonly }) {
    const onDeleteClick = (0, react_1.useCallback)(() => !readonly && onDelete(enu), [enu, onDelete, readonly]);
    return (React.createElement("li", { id: id ? `_laji-form_${id}` : undefined, key: enu.value, style: { display: "inline-table" }, className: (0, utils_1.classNames)("laji-form-multiselect-tag", className) },
        enu.label,
        React.createElement("span", { tabIndex: readonly ? undefined : 0, role: readonly ? undefined : "button", onClick: onDeleteClick }, "\u00D7")));
}
const Caret = () => (React.createElement("div", { className: "laji-form-dropdown-caret-container", style: { position: "absolute", pointerEvents: "none" } },
    React.createElement("span", { className: "laji-form-dropdown-caret" },
        React.createElement("img", { src: "https://cdn.laji.fi/images/icons/caret-down.svg" }))));
function ListItem({ onSelected, active, children }) {
    const onClick = (0, react_1.useCallback)(() => {
        onSelected(children);
    }, [children, onSelected]);
    return (React.createElement("div", { onClick: onClick, className: (0, utils_1.classNames)("laji-form-dropdown-item", active && "active"), tabIndex: -1 }, children.label));
}
function getDefaultActiveIdx(filteredEnums, value) {
    return value !== undefined && value !== ""
        ? filteredEnums.findIndex(item => item.value === value)
        : 0;
}
