Manual Reference Source

basic/form/InputJson.js

/**
 * Created by jakubniezgoda on 11/04/2019.
 */

import PropTypes from 'prop-types';

import React, { Component } from 'react';
import ReactJsonView from 'react-json-view';
import { TextArea } from 'semantic-ui-react';

import { Icon, Label, List, Popup } from '../index';

class ReactJsonViewWrapper extends Component {
    static propTypes = {
        value: PropTypes.object,
        divStyle: PropTypes.object,
        onChange: PropTypes.func
    };

    static defaultProps = {
        value: {},
        divStyle: {},
        onChange: _.noop
    };

    shouldComponentUpdate(nextProps) {
        return !_.isEqual(nextProps, this.props);
    }

    render() {
        return (
            <div style={this.props.divStyle}>
                <ReactJsonView
                    src={this.props.value}
                    name={null}
                    enableClipboard={false}
                    defaultValue=""
                    onAdd={this.props.onChange}
                    onEdit={this.props.onChange}
                    onDelete={this.props.onChange}
                />
            </div>
        );
    }
}

/**
 * InputJson is a component providing text or rich editor for JSON-like data
 *
 * ## Access
 * `Stage.Basic.Form.Json`
 *
 * ## Usage
 * ![InputJson](manual/asset/form/InputJson_0.png)
 *
 * ```
 * <Form.Json name='port_conf' value={'{"webserver_port2":6,"webserver_port1":5}'} />
 * ```
 *
 */
export default class InputJson extends Component {
    constructor(props, context) {
        super(props, context);

        this.state = {
            isRawView: true,
            isParsableToJson: false,
            isMouseOver: false
        };

        this.onChangeJson = this.onChangeJson.bind(this);
        this.onChangeString = this.onChangeString.bind(this);
        this.switchView = this.switchView.bind(this);
    }

    /**
     * propTypes
     *
     * @property {string} name name of the field
     * @property {any} [value="{}"] value of the field
     * @property {boolean} [error=false] is field invalid
     * @property {Function} [onChange=(function () {});] function to be called on value change
     */
    static propTypes = {
        name: PropTypes.string.isRequired,
        value: PropTypes.any,
        error: PropTypes.bool,
        onChange: PropTypes.func
    };

    static defaultProps = {
        value: '{}',
        error: false,
        onChange: _.noop
    };

    shouldComponentUpdate(nextProps, nextState) {
        return !_.isEqual(nextProps, this.props) || !_.isEqual(nextState, this.state);
    }

    componentDidMount() {
        const isParsableToJson = this.isParsableToJson();
        this.setState({ isParsableToJson, isRawView: !isParsableToJson });
    }

    componentDidUpdate(prevProps) {
        if (!_.isEqual(this.props.value, prevProps.value)) {
            const isParsableToJson = this.isParsableToJson();
            this.setState({
                isParsableToJson,
                isRawView: !this.state.isRawView ? !isParsableToJson : this.state.isRawView
            });
        }
    }

    // See: https://stackoverflow.com/questions/9804777/how-to-test-if-a-string-is-json-or-not
    isParsableToJson() {
        const { value } = this.props;
        let isParsableToJson = true;

        if (!_.isString(value)) {
            isParsableToJson = false;
        } else {
            try {
                const result = JSON.parse(value);
                isParsableToJson =
                    Object.prototype.toString.call(result) === '[object Object]' || Array.isArray(result);
            } catch (err) {
                isParsableToJson = false;
            }
        }

        return isParsableToJson;
    }

    onChangeJson(changeObject) {
        const { Json } = Stage.Utils;

        this.props.onChange(null, {
            name: this.props.name,
            value: Json.getStringValue(changeObject.updated_src)
        });
    }

    onChangeString(event, { name, value }) {
        this.props.onChange(event, { name, value });
    }

    switchView() {
        this.setState({ isRawView: !this.state.isRawView });
    }

    render() {
        const { Json } = Stage.Utils;

        const { value } = this.props;
        const stringValue = Json.getStringValue(value);
        const jsonValue = Json.getTypedValue(value);

        const divStyle = {
            backgroundColor: this.props.error ? '#fff6f6' : '',
            border: `1px solid ${this.props.error ? 'rgb(224, 180, 180)' : 'rgba(34,36,38,.15)'}`,
            borderRadius: 4,
            padding: 10
        };

        return (
            <div
                style={{ position: 'relative' }}
                onMouseEnter={() => this.setState({ isMouseOver: true })}
                onMouseLeave={() => this.setState({ isMouseOver: false })}
            >
                {this.state.isRawView ? (
                    <TextArea name={this.props.name} value={stringValue} onChange={this.onChangeString} />
                ) : (
                    <ReactJsonViewWrapper value={jsonValue} divStyle={divStyle} onChange={this.onChangeJson} />
                )}
                {this.state.isMouseOver && (
                    <Popup>
                        <Popup.Trigger>
                            <Icon
                                name="edit"
                                link={this.state.isParsableToJson}
                                disabled={!this.state.isParsableToJson}
                                style={{ position: 'absolute', top: 10, right: 30 }}
                                onClick={this.state.isParsableToJson ? this.switchView : _.noop}
                            />
                        </Popup.Trigger>
                        <Popup.Content>
                            {this.state.isParsableToJson
                                ? `Switch to ${this.state.isRawView ? 'Rich View' : 'Text View'}`
                                : 'Cannot switch to Rich View. Text cannot be parsed to JSON.'}
                        </Popup.Content>
                    </Popup>
                )}
                {this.state.isMouseOver && !this.state.isRawView && (
                    <Popup wide="very">
                        <Popup.Trigger>
                            <Icon name="info" style={{ position: 'absolute', top: 10, right: 50, cursor: 'pointer' }} />
                        </Popup.Trigger>
                        <Popup.Content>
                            <List>
                                <List.Item>
                                    <Label>Ctrl + Click</Label> to enter edit mode
                                </List.Item>
                                <List.Item>
                                    In edit mode <Label>Ctrl + Enter</Label> to submit changes
                                </List.Item>
                                <List.Item>
                                    In edit mode <Label>Escape</Label> to cancel changes
                                </List.Item>
                            </List>
                        </Popup.Content>
                    </Popup>
                )}
            </div>
        );
    }
}