wurmlab/sequenceserver

View on GitHub
public/js/search_button.js

Summary

Maintainability
A
2 hrs
Test Coverage
F
55%
import React, { Component, createRef } from 'react';
import _ from 'underscore';

/**
 * SearchButton widget.
 */
export class SearchButton extends Component {
    constructor(props) {
        super(props);
        this.state = {
            methods: [],
            hasQuery: false,
            hasDatabases: false,
            dropdownVisible: false,
            messageTooltip: "",
            titleTooltip: "",
            hideMessageTooltip: false
        };
        this.changeAlgorithm = this.changeAlgorithm.bind(this);
        this.decorate = this.decorate.bind(this);
        this.setMessageTooltipButton = this.setMessageTooltipButton.bind(this);
        this.setTitleTooltipButton = this.setTitleTooltipButton.bind(this);
        this.renderButton = this.renderButton.bind(this);
        this.inputGroup = this.inputGroup.bind(this);
        this.handleMouseOver = this.handleMouseOver.bind(this)
        this.handeMouseOut = this.handeMouseOut.bind(this)
        this.inputGroupRef = createRef();
    }

    componentDidMount() {
        this.timeout = setTimeout(() => this.setState({ hideMessageTooltip: true}), 3000)
    }

    shouldComponentUpdate(props, state) {
        return !_.isEqual(state.methods, this.state.methods) ||
            state.dropdownVisible !== this.state.dropdownVisible ||
            state.messageTooltip !== this.state.messageTooltip ||
            state.titleTooltip !== this.state.titleTooltip ||
            state.hideMessageTooltip !== this.state.hideMessageTooltip;
    }

    componentDidUpdate(_prevProps, prevState) {
        if (!_.isEqual(prevState.methods, this.state.methods)) {
            if (this.state.methods.length > 0) {
                this.inputGroup().wiggle();
                this.props.onAlgoChanged(this.state.methods[0]);
            } else {
                this.props.onAlgoChanged('');
            }
        }
    }

    inputGroup() {
        return $(this.inputGroupRef.current);
    }

    /**
 * Change selected algorithm.
 *
 * NOTE: Called on click on dropdown menu items.
 */
    changeAlgorithm(method) {
        var methods = this.state.methods.slice();
        methods.splice(methods.indexOf(method), 1);
        methods.unshift(method);
        this.setState({
            methods: methods,
            dropdownVisible: false,
        });
    }

    /**
 * Given, for example 'blastp', returns blast<strong>p</strong>.
 */
    decorate(name) {
        return name
            .match(/(.?)(blast)(.?)/)
            .slice(1)
            .map(function (token, _) {
                if (token) {
                    if (token !== 'blast') {
                        return <strong key={token}>{token}</strong>;
                    } else {
                        return token;
                    }
                }
            });
    }

    toggleDropdownVisibility = () => {
        this.setState(prevState => ({
            dropdownVisible: !prevState.dropdownVisible
        }));
    }
    
    setMessageTooltipButton() {
        if (!this.state.hasQuery && !this.state.hasDatabases) {
            this.setState({ messageTooltip: 'You must enter a query sequence and select one or more databases above before you can run a search!' });
        } else if (this.state.hasQuery && !this.state.hasDatabases) {
            this.setState({ messageTooltip: 'You must select one or more databases above before you can run a search!' });
        } else if (!this.state.hasQuery && this.state.hasDatabases) {
            this.setState({ messageTooltip: 'You must enter a query sequence above before you can run a search!' });
        }
    }

    setTitleTooltipButton() {
        var title = 'Click to BLAST or press Ctrl+Enter.';

        this.setState({ titleTooltip: title });

        if (this.state.methods.length > 1) {
            this.setState({
                titleTooltip: title +=
                    ' Click dropdown button on the right for other' +
                    ' BLAST algorithms that can be used.'
             });
        }
    }

    handeMouseOut() {
        clearTimeout(this.timeout);
        this.setState({ hideMessageTooltip: false });
    }

    handleMouseOver() {
        this.setMessageTooltipButton();
        this.setTitleTooltipButton();
        this.timeout = setTimeout(() => this.setState({ hideMessageTooltip: true}), 3000)
    }

    renderButton(method, multi, methods) {
        return (
            <div className="flex">
                <button
                    type="submit"
                    className={`${method ? 'hover:bg-seqorange focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-seqorange ' : 'opacity-50 cursor-not-allowed '}uppercase w-full md:w-auto flex text-xl justify-center py-2 px-16 border border-transparent rounded-md shadow-sm text-white bg-seqblue`}
                    id="method"
                    name="method"
                    value={method}
                    disabled={!method}
                >
                    {this.decorate(method || 'blast')}
                </button>

                {multi && (
                    <div className="ui--multi-dropdown">
                        <button
                            className="text-xl bg-seqblue rounded-r-md text-white p-2 border border-seqblue hover:bg-seqorange focus:outline-none focus:ring-1 focus:ring-seqorange -ml-8"
                            type="button"
                            onClick={this.toggleDropdownVisibility}
                        >
                            <i className="fas fa-caret-down w-6 h-6 fill-current"></i>
                            <span className="sr-only">Other methods</span>
                        </button>

                        <div id="dropdown"
                            className={`z-10 my-2 uppercase bg-blue-300 divide-y divide-gray-100 rounded-lg shadow absolute left-0 bottom-12 w-full text-xl text-center ${this.state.dropdownVisible ? '' : 'hidden'}`}>
                            <ul className="text-gray-700" aria-labelledby="dropdownDefaultButton">
                                {_.map(
                                    methods.slice(1),
                                    _.bind(function (method) {
                                        return (
                                            <li
                                                key={method}
                                                onClick={_.bind(function () {
                                                    this.changeAlgorithm(method);
                                                }, this)}
                                            >
                                                <a href="#" className="block px-4 py-2 hover:bg-blue-400 rounded-lg">{method}</a>
                                            </li>
                                        );
                                    }, this)
                                )}
                            </ul>
                        </div>
                    </div>
                )}
            </div>
        )
    }

    render() {
        var methods = this.state.methods;
        var method = methods[0];
        var multi = methods.length > 1;

        return (
            <div
                className="my-4 md:my-2 flex justify-end w-full md:w-auto relative"
                id="methods"
                ref={this.inputGroupRef}
                onMouseOver={this.handleMouseOver}
                onMouseOut={this.handeMouseOut}
            >
                <div className="relative flex flex-col items-center group">
                    <div className="flex items-center w-full">
                        {this.renderButton(method, multi, methods)}
                        { !this.state.dropdownVisible &&
                            <div className="absolute hidden bottom-11 items-center flex-col-reverse group-hover:flex w-full">
                                <div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mt-1 -mr-px tooltip-arrow-rotate"></div>
                                <span className="relative z-10 p-2 search-button-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
                                    { !this.state.hasQuery || !this.state.hasDatabases ? (
                                        this.state.hideMessageTooltip ?  <>{this.state.titleTooltip}</> : this.state.messageTooltip
                                    ) : <> {this.state.titleTooltip}</>}
                                </span>
                            </div>
                        }
                    </div>
                </div>
            </div>
        );
    }
}