/*
 * Copyright 2017 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { __decorate } from "tslib";
import classNames from "classnames";
import * as React from "react";
import { polyfill } from "react-lifecycles-compat";
import { AbstractPureComponent2, Classes, Keys, Utils } from "../../common";
import { DISPLAYNAME_PREFIX } from "../../common/props";
import { Icon } from "../icon/icon";
import { Tag } from "../tag/tag";
/** special value for absence of active tag */
const NONE = -1;
let TagInput = /** @class */ (() => {
    let TagInput = class TagInput extends AbstractPureComponent2 {
        constructor() {
            super(...arguments);
            this.state = {
                activeIndex: NONE,
                inputValue: this.props.inputValue || "",
                isInputFocused: false,
            };
            this.refHandlers = {
                input: (ref) => {
                    this.inputElement = ref;
                    Utils.safeInvoke(this.props.inputRef, ref);
                },
            };
            this.addTags = (value, method = "default") => {
                const { inputValue, onAdd, onChange, values } = this.props;
                const newValues = this.getValues(value);
                let shouldClearInput = Utils.safeInvoke(onAdd, newValues, method) !== false && inputValue === undefined;
                // avoid a potentially expensive computation if this prop is omitted
                if (Utils.isFunction(onChange)) {
                    shouldClearInput = onChange([...values, ...newValues]) !== false && shouldClearInput;
                }
                // only explicit return false cancels text clearing
                if (shouldClearInput) {
                    this.setState({ inputValue: "" });
                }
            };
            this.maybeRenderTag = (tag, index) => {
                if (!tag) {
                    return null;
                }
                const { large, tagProps } = this.props;
                const props = Utils.isFunction(tagProps) ? tagProps(tag, index) : tagProps;
                return (React.createElement(Tag, Object.assign({ active: index === this.state.activeIndex, "data-tag-index": index, key: tag + "__" + index, large: large, onRemove: this.props.disabled ? null : this.handleRemoveTag }, props), tag));
            };
            this.handleContainerClick = () => {
                if (this.inputElement != null) {
                    this.inputElement.focus();
                }
            };
            this.handleContainerBlur = ({ currentTarget }) => {
                requestAnimationFrame(() => {
                    // we only care if the blur event is leaving the container.
                    // defer this check using rAF so activeElement will have updated.
                    if (!currentTarget.contains(document.activeElement)) {
                        if (this.props.addOnBlur && this.state.inputValue !== undefined && this.state.inputValue.length > 0) {
                            this.addTags(this.state.inputValue, "blur");
                        }
                        this.setState({ activeIndex: NONE, isInputFocused: false });
                    }
                });
            };
            this.handleInputFocus = (event) => {
                this.setState({ isInputFocused: true });
                Utils.safeInvoke(this.props.inputProps.onFocus, event);
            };
            this.handleInputChange = (event) => {
                this.setState({ activeIndex: NONE, inputValue: event.currentTarget.value });
                Utils.safeInvoke(this.props.onInputChange, event);
                Utils.safeInvoke(this.props.inputProps.onChange, event);
            };
            this.handleInputKeyDown = (event) => {
                const { selectionEnd, value } = event.currentTarget;
                const { activeIndex } = this.state;
                let activeIndexToEmit = activeIndex;
                if (event.which === Keys.ENTER && value.length > 0) {
                    this.addTags(value, "default");
                }
                else if (selectionEnd === 0 && this.props.values.length > 0) {
                    // cursor at beginning of input allows interaction with tags.
                    // use selectionEnd to verify cursor position and no text selection.
                    if (event.which === Keys.ARROW_LEFT || event.which === Keys.ARROW_RIGHT) {
                        const nextActiveIndex = this.getNextActiveIndex(event.which === Keys.ARROW_RIGHT ? 1 : -1);
                        if (nextActiveIndex !== activeIndex) {
                            event.stopPropagation();
                            activeIndexToEmit = nextActiveIndex;
                            this.setState({ activeIndex: nextActiveIndex });
                        }
                    }
                    else if (event.which === Keys.BACKSPACE) {
                        this.handleBackspaceToRemove(event);
                    }
                    else if (event.which === Keys.DELETE) {
                        this.handleDeleteToRemove(event);
                    }
                }
                this.invokeKeyPressCallback("onKeyDown", event, activeIndexToEmit);
            };
            this.handleInputKeyUp = (event) => {
                this.invokeKeyPressCallback("onKeyUp", event, this.state.activeIndex);
            };
            this.handleInputPaste = (event) => {
                const { separator } = this.props;
                const value = event.clipboardData.getData("text");
                if (!this.props.addOnPaste || value.length === 0) {
                    return;
                }
                // special case as a UX nicety: if the user pasted only one value with no delimiters in it, leave that value in
                // the input field so that the user can refine it before converting it to a tag manually.
                if (separator === false || value.split(separator).length === 1) {
                    return;
                }
                event.preventDefault();
                this.addTags(value, "paste");
            };
            this.handleRemoveTag = (event) => {
                // using data attribute to simplify callback logic -- one handler for all children
                const index = +event.currentTarget.parentElement.getAttribute("data-tag-index");
                this.removeIndexFromValues(index);
            };
        }
        static getDerivedStateFromProps(props, state) {
            if (props.inputValue !== state.prevInputValueProp) {
                return {
                    inputValue: props.inputValue,
                    prevInputValueProp: props.inputValue,
                };
            }
            return null;
        }
        render() {
            const { className, disabled, fill, inputProps, intent, large, leftIcon, placeholder, values } = this.props;
            const classes = classNames(Classes.INPUT, Classes.TAG_INPUT, {
                [Classes.ACTIVE]: this.state.isInputFocused,
                [Classes.DISABLED]: disabled,
                [Classes.FILL]: fill,
                [Classes.LARGE]: large,
            }, Classes.intentClass(intent), className);
            const isLarge = classes.indexOf(Classes.LARGE) > NONE;
            // use placeholder prop only if it's defined and values list is empty or contains only falsy values
            const isSomeValueDefined = values.some(val => !!val);
            const resolvedPlaceholder = placeholder == null || isSomeValueDefined ? inputProps.placeholder : placeholder;
            return (React.createElement("div", { className: classes, onBlur: this.handleContainerBlur, onClick: this.handleContainerClick },
                React.createElement(Icon, { className: Classes.TAG_INPUT_ICON, icon: leftIcon, iconSize: isLarge ? Icon.SIZE_LARGE : Icon.SIZE_STANDARD }),
                React.createElement("div", { className: Classes.TAG_INPUT_VALUES },
                    values.map(this.maybeRenderTag),
                    this.props.children,
                    React.createElement("input", Object.assign({ value: this.state.inputValue }, inputProps, { onFocus: this.handleInputFocus, onChange: this.handleInputChange, onKeyDown: this.handleInputKeyDown, onKeyUp: this.handleInputKeyUp, onPaste: this.handleInputPaste, placeholder: resolvedPlaceholder, ref: this.refHandlers.input, className: classNames(Classes.INPUT_GHOST, inputProps.className), disabled: disabled }))),
                this.props.rightElement));
        }
        getNextActiveIndex(direction) {
            const { activeIndex } = this.state;
            if (activeIndex === NONE) {
                // nothing active & moving left: select last defined value. otherwise select nothing.
                return direction < 0 ? this.findNextIndex(this.props.values.length, -1) : NONE;
            }
            else {
                // otherwise, move in direction and clamp to bounds.
                // note that upper bound allows going one beyond last item
                // so focus can move off the right end, into the text input.
                return this.findNextIndex(activeIndex, direction);
            }
        }
        findNextIndex(startIndex, direction) {
            const { values } = this.props;
            let index = startIndex + direction;
            while (index > 0 && index < values.length && !values[index]) {
                index += direction;
            }
            return Utils.clamp(index, 0, values.length);
        }
        /**
         * Splits inputValue on separator prop,
         * trims whitespace from each new value,
         * and ignores empty values.
         */
        getValues(inputValue) {
            const { separator } = this.props;
            // NOTE: split() typings define two overrides for string and RegExp.
            // this does not play well with our union prop type, so we'll just declare it as a valid type.
            return (separator === false ? [inputValue] : inputValue.split(separator))
                .map(val => val.trim())
                .filter(val => val.length > 0);
        }
        handleBackspaceToRemove(event) {
            const previousActiveIndex = this.state.activeIndex;
            // always move leftward one item (this will focus last item if nothing is focused)
            this.setState({ activeIndex: this.getNextActiveIndex(-1) });
            // delete item if there was a previous valid selection (ignore first backspace to focus last item)
            if (this.isValidIndex(previousActiveIndex)) {
                event.stopPropagation();
                this.removeIndexFromValues(previousActiveIndex);
            }
        }
        handleDeleteToRemove(event) {
            const { activeIndex } = this.state;
            if (this.isValidIndex(activeIndex)) {
                event.stopPropagation();
                this.removeIndexFromValues(activeIndex);
            }
        }
        /** Remove the item at the given index by invoking `onRemove` and `onChange` accordingly. */
        removeIndexFromValues(index) {
            const { onChange, onRemove, values } = this.props;
            Utils.safeInvoke(onRemove, values[index], index);
            if (Utils.isFunction(onChange)) {
                onChange(values.filter((_, i) => i !== index));
            }
        }
        invokeKeyPressCallback(propCallbackName, event, activeIndex) {
            Utils.safeInvoke(this.props[propCallbackName], event, activeIndex === NONE ? undefined : activeIndex);
            Utils.safeInvoke(this.props.inputProps[propCallbackName], event);
        }
        /** Returns whether the given index represents a valid item in `this.props.values`. */
        isValidIndex(index) {
            return index !== NONE && index < this.props.values.length;
        }
    };
    TagInput.displayName = `${DISPLAYNAME_PREFIX}.TagInput`;
    TagInput.defaultProps = {
        addOnBlur: false,
        addOnPaste: true,
        inputProps: {},
        separator: /[,\n\r]/,
        tagProps: {},
    };
    TagInput = __decorate([
        polyfill
    ], TagInput);
    return TagInput;
})();
export { TagInput };
//# sourceMappingURL=tagInput.js.map