matthew-matvei/freeman

View on GitHub
src/renderer/components/blocks/InputItem.tsx

Summary

Maintainability
B
4 hrs
Test Coverage
import autobind from "autobind-decorator";
import * as React from "react";
import { HotKeys } from "react-hotkeys";

import { DirectoryItemIcon } from "components/blocks";
import LoggedError from "errors/LoggedError";
import { IHandlers } from "models";
import { IInputItemProps } from "props/blocks";
import { IInputItemState } from "states/blocks";

import "styles/blocks/InputItem.scss";

/** The input component to create a new directory item. */
class InputItem extends React.Component<IInputItemProps, IInputItemState> {

    /** Handler functions for the given events this component handles. */
    private handlers: IHandlers = {
        moveBack: () => { },
        moveDown: () => { },
        moveUp: () => { }
    };

    /** A reference to this input element. */
    private input?: HTMLInputElement | null;

    /**
     * Instantiates the InputItem component.
     *
     * @param props the properties for the DirectoryPane component
     */
    public constructor(props: IInputItemProps) {
        super(props);

        this.state = {
            isInvalid: false
        };
    }

    /** Sets the focus to this item immediately after mounting. */
    public componentDidMount() {
        this.input && this.input.focus();
    }

    /**
     * Returns whether the component should update.
     *
     * @param nextProps the next props
     * @param nextState the next state
     *
     * @returns whether the component should update
     */
    public shouldComponentUpdate(nextProps: {}, nextState: IInputItemState): boolean {
        return this.state.isInvalid !== nextState.isInvalid;
    }

    /**
     * Defines how the input item component is rendered.
     *
     * @returns a JSX element representing the directory view
     */
    public render(): JSX.Element {
        const inputStyle: React.CSSProperties = {
            border: this.state.isInvalid ?
                `2px solid ${this.props.theme.inputItem.invalidInput}` :
                `2px solid ${this.props.theme.primaryBackgroundColour}`
        };

        const icon = this.props.creatingItemType &&
            <DirectoryItemIcon
                directoryItemType={this.props.creatingItemType}
                theme={this.props.theme} />;

        return (
            <HotKeys handlers={this.handlers}>
                <div className="Item InputItem">
                    {icon}
                    <input type="text"
                        style={inputStyle}
                        ref={input => this.input = input}
                        onKeyUp={this.handleKeyUp} />
                </div>
            </HotKeys>);
    }

    /**
     * Handles interpreting the input value on key up.
     *
     * @param event an event raised on key up
     */
    @autobind
    private handleKeyUp(event: React.KeyboardEvent<HTMLInputElement>) {
        if (!this.input) {
            return;
        }

        if (this.props.sendUpCreateItem) {
            return this.handleCreate(event);
        } else if (this.props.sendUpRenameItem) {
            return this.handleRename(event);
        }

        throw new LoggedError("No callback function passed to InputItem component");
    }

    /**
     * Handles sending a create item request up to the parent component.
     *
     * @param event an event raised on key up
     *
     * @require this.input && this.props.sendUpCreateItem
     */
    private handleCreate(event: React.KeyboardEvent<HTMLInputElement>) {
        if (event.key === "Escape") {
            return this.props.sendUpCreateItem!();
        }

        if (!this.validate(this.input!.value)) {
            this.setState({ isInvalid: true } as IInputItemState);

            return;
        } else {
            this.setState({ isInvalid: false } as IInputItemState);
        }

        if (event.key === "Enter") {
            return this.props.sendUpCreateItem!(this.input!.value, this.props.creatingItemType);
        }
    }

    /**
     * Handles sending a rename item request up to the parent component.
     *
     * @param event an event raised on key up
     *
     * @require this.input && this.props.sendUpRenameItem
     */
    private handleRename(event: React.KeyboardEvent<HTMLInputElement>) {
        if (event.key === "Escape") {
            return this.props.sendUpRenameItem!();
        }

        if (!this.validate(this.input!.value)) {
            this.setState({ isInvalid: true } as IInputItemState);

            return;
        } else {
            this.setState({ isInvalid: false } as IInputItemState);
        }

        if (event.key === "Enter") {
            if (!this.props.thisItem) {
                throw new LoggedError("thisItem is not defined");
            }

            return this.props.sendUpRenameItem!(this.props.thisItem.name, this.input!.value);

        }
    }

    /**
     * Validates the given content.
     *
     * @param content the content to validate
     *
     * @returns whether the given content is valid
     */
    private validate(content: string): boolean {
        if (!content) {
            return false;
        }

        if (this.props.otherItems.some(item => item.name === content)) {
            return false;
        }

        return true;
    }
}

export default InputItem;