cmspsgp31/anubis

View on GitHub
anubis/frontend/src/components/TokenField/token_list.js

Summary

Maintainability
D
1 day
Test Coverage
// Copyright © 2016, Ugo Pozo
//             2016, Câmara Municipal de São Paulo

// token_list.js - a list of tokens in a TokenField.

// This file is part of Anubis.

// Anubis is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Anubis is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.


import React from 'react';
import ReactDOM from 'react-dom';
import I from 'immutable';
import IPropTypes from 'react-immutable-proptypes';
import _ from 'lodash';

import {PropTypes as RPropTypes} from 'react';

import EditorToken from './editor_token';
import SymbolTokens from './symbol_token';
import UnitToken from './unit_token';

export default class TokenList extends React.Component {
    static propTypes = {
        canSearch: RPropTypes.bool,
        createTokenEditor: RPropTypes.func,
        defaultFilter: RPropTypes.string,
        deleteTokenEditor: RPropTypes.func,
        expandDefaultUnitEditor: RPropTypes.func,
        expression: IPropTypes.listOf(
            IPropTypes.contains({
                key: RPropTypes.string.isRequired,
                index: RPropTypes.number.isRequired,
                args: IPropTypes.listOf(RPropTypes.string),
            }),
        ),
        fieldsets: IPropTypes.mapContains({
            description: RPropTypes.string,
            fields: IPropTypes.listOf(
                IPropTypes.mapContains({
                    choices: IPropTypes.list,
                    help_text: RPropTypes.string,
                    is_numeric: RPropTypes.bool.isRequired,
                    label: RPropTypes.string,
                    required: RPropTypes.bool.isRequired,
                    ui_element: RPropTypes.string.isRequired,
                }),
            ),
        }),
        focus: RPropTypes.func,
        isFocused: RPropTypes.func,
        modifyInnerFieldEditor: RPropTypes.func,
        moveTokenEditor: RPropTypes.func,
        onBlur: RPropTypes.func,
        onChange: RPropTypes.func,
        onFocus: RPropTypes.func,
        onSearch: RPropTypes.func.isRequired,
        onUpdate: RPropTypes.func,
        position: RPropTypes.number,
        reorderTokensEditor: RPropTypes.func,
        textElement: RPropTypes.func,
        value: RPropTypes.string,
    };

    static buildNewToken(key, meta) {
        let token = I.fromJS({
            key,
        });

        if (meta) {
            token = token.set('args', meta.get('fields').map(f => {
                let arg = null;
                if (f.get('ui_element') == "SelectField") {
                    arg = f.getIn(['choices', 0, 0]);
                }
                else {
                    arg = f.get('initial');
                }

                return `${arg}`;
            }));
        }

        return token;
    }

    static connectToken(created, prev, next) {
        if (prev == null) prev = I.fromJS({key: "__AND__"});
        if (next == null) next = I.fromJS({key: "__AND__"});

        const isCreatedUnitToken = !_.has(this.connectorMap,
            created.get('key'));

        const prevRequiresConnector = !_.has(this.connectorMap,
            prev.get('key')) || (prev.get('key') == "__RPARENS__");

        const nextRequiresConnector = !_.has(this.connectorMap,
            next.get('key')) || (next.get('key') == "__LPARENS__");

        const tokens = [created];

        if (isCreatedUnitToken && prevRequiresConnector) {
            tokens.splice(0, 0, I.fromJS({
                key: "__AND__",
            }));
        }

        if (isCreatedUnitToken && nextRequiresConnector) {
            tokens.splice(tokens.length, 0, I.fromJS({
                key: "__AND__",
            }));
        }

        return tokens;
    }

    static connectorMap = {
        "__AND__": "/",
        "__OR__": "+",
        "__NOT__": "!",
        "__LPARENS__": "(",
        "__RPARENS__": ")",
    }


    constructor(props) {
        super(props);

        this.state = {
            tokens: [],
            editorValue: "",
        };
    }

    componentWillMount() {
        this.getTokens();
    }

    componentDidUpdate(prevProps) {
        const diff = this.areExprsDiff(prevProps.expression,
            this.props.expression);

        if (diff) {
            this.getTokens();
        }
    }

    areExprsDiff(prev, current) {
        if (prev.size != current.size) return true;

        return _.zip(prev.toJS(), current.toJS()).some(([p, c]) => {
            if (p.key != c.key) return true;

            if ((!!p.args && !c.args) || (!p.args && !!c.args)) return true;

            if (!p.args) return false;

            if (p.args.length != c.args.length) return true;

            return _.zip(p.args, c.args).some(([pa, ca]) => pa != ca);
        });
    }

    get inputProps() {
        const {onBlur, onFocus} = this.props;

        return {onBlur, onChange: this.handleChange, onFocus};
    }

    handleChange = ev => {
        const effectiveValue = this.props.value + ev.target.value;

        this.editorValue = ev.target.value;

        return this.props.onChange({target: {value: effectiveValue}});
    }

    get editorValue() {
        return this.state.editorValue;
    }

    set editorValue(value) {
        this.setState({editorValue: value});
    }

    handleSort = (dragged, over, overOffset) => {
        if (dragged == over) return;

        let initOrder = this.props.expression.map(obj => obj.get('index'));

        initOrder = initOrder.insert(this.props.position, '__EDITOR__');

        let [overPos] = initOrder.findEntry(v => v == over);
        const [draggedPos] = initOrder.findEntry(v => v == dragged);

        if (draggedPos < overPos) overPos--;

        overPos += overOffset;

        const finalOrder = initOrder.remove(draggedPos).insert(overPos,
                                                               dragged);


        if (!I.is(initOrder, finalOrder)) {
            this.props.reorderTokensEditor(finalOrder.toArray());
        }
    }

    getTokens() {
        this.tokenObjects = [];

        this.setState({tokens: this.props.expression.map((obj, i) => {
            const key = obj.get('key');
            let fieldset = this.props.fieldsets.get(key);
            let values = obj.get('args', null);
            const id = obj.get('index');

            if (!fieldset) fieldset = {key};
            else fieldset = fieldset.toJS();

            if (values && values.toJS) values = values.toJS();

            const props = {
                ...fieldset,
                values,
                searchKey: key,
                index: i,
                id,
                onRemove: this.props.deleteTokenEditor,
                onChange: this.props.modifyInnerFieldEditor,
                onSearch: this.props.onSearch,
                onSort: this.handleSort,
                setEditorValue: v => this.editorValue = v,
                textElement: this.props.textElement,
                token: obj,
            };

            if (_.has(SymbolTokens, key)) {
                const tokenCls = SymbolTokens[key];

                return React.createElement(tokenCls, {
                    ...props,
                    key: `token_${id}`,
                    sortData: `${id}`,
                    ref: t => this.tokenObjects.push(t),
                });
            }
            else return (
                <UnitToken
                    {...props}
                    key={`token_${id}`}
                    ref={t => this.tokenObjects.push(t)}
                    sortData={`${id}`}
                />
            );
        }).toJS()});

        this.props.onUpdate();
    }

    getInputNode() {
        return {
            blur: () => this.editorToken.lead.blur(),
            focus: () => this.editorToken.lead.focus(),
        };
    }

    get shouldDisable() {
        return false;
    }

    render() {
        const styles = this.state.tokens.map(token => ({
            key: `token_${token.props.id}`,
            data: token,
            style: {
                // opacity: spring(1, {precision: 0.1}),
                // left: spring(0, {precision: 10}),
            },
        }));

        styles.splice(this.props.position, 0, {
            key: "editor_token",
            style: {},
            data: React.createElement(EditorToken, {
                deleteToken: this.props.deleteTokenEditor,
                disabled: this.shouldDisable,
                expressionSize: this.props.expression.size,
                inputProps: this.inputProps,
                insertToken: this.props.createTokenEditor,
                expandDefaultUnit: this.props.expandDefaultUnitEditor,
                isFocused: this.props.isFocused,
                move: this.props.moveTokenEditor,
                setEditorValue: v => this.editorValue = v,
                onSearch: this.props.onSearch,
                onSort: this.handleSort,
                position: this.props.position,
                ref: c => this.editorToken = (c) ?
                    c.getDecoratedComponentInstance() :
                    null,
                sortData: "__EDITOR__",
                units: this.props.fieldsets,
                value: this.editorValue,
            }),
        });

        return (
            <div
                onTouchTap={ev => {
                    const elem = ReactDOM.findDOMNode(this.mainEditingArea);

                    if (ev.target == elem) {
                        this.props.focus();
                    }
                }}
                ref={c => this.mainEditingArea = c}
                style={{
                    display: "flex",
                    flexFlow: "row wrap",
                    justifyContent: "flex-start",
                    alignContent: "flex-start",
                    paddingTop: "45px",
                    paddingBottom: "10px",
                }}
            >
                {styles.map(s =>
                    React.cloneElement(s.data, {
                        // style: s.style,
                        key: s.key,
                    })
                )}
            </div>
        );

        // return (
        //  <TransitionMotion
        //      styles={styles}
        //      willEnter={() => ({
        //          opacity: 0,
        //          left: -100,
        //      })}
        //      willLeave={() => ({
        //          opacity: spring(0, {precision: 0.1}),
        //          left: spring(100, {precision: 10}),
        //      })}
        //  >
        //      {styles =>
        //      }
        //  </TransitionMotion>
        // );
    }

}