Home Reference Source

application/components/common/rich-editor2/plugins/suggestion.js

import React from 'react';
import ToolbarButton from '../common/toolbar-button';
import CurrentUser from '../../../../current-user';
import Immutable from 'immutable';
import Dialog from 'material-ui/Dialog';
import TextField from 'material-ui/TextField';
import CheckBox from 'material-ui/Checkbox';
import FlatButton from 'material-ui/FlatButton';
import RaisedButton from 'material-ui/RaisedButton';

export const SERIALIZER_RULES = [
];

let keywords;
let related;
let isActive;

class SuggestionButton extends ToolbarButton
{
    handleOnClick(e) {
        return suggestionClickHandler.call(this.props.editor, e);
    }

    getLabel() {
        return <i class="fa fa-retweet"></i>;
    }
}

class SuggestionNode extends React.Component
{
    constructor(props) {
        super(props);

        this.parentEditor = this.props.editor;
    }

    render() {

        const handleClick = e => {
            e.preventDefault();

            // open the modal and store the anchor data
            this.props.editor.setState({
                'mySuggestionModalOpen': true,
                'mySuggestionKeyword': this.props.children
            });
        }


        return (
            <span {...this.props.attributes} style={{color: 'green', textDecoration: 'underline', cursor: 'pointer'}} onClick={handleClick}>
                {this.props.children}
            </span>
        );
    }
}

function suggestionClickHandler(e) {
    const {editor} = this;
    const {value} = editor;

    const texts = value.document.getTexts();
    const decorations = [];
    if (isActive) {
        isActive = false;
        editor.withoutSaving(() => {
            editor.setDecorations(decorations);
        });
        return;
    }

    isActive = true;

    texts.forEach(node => {
        const {key, text} = node;

        keywords.forEach(keyword => {

            let hasRelated = false;
            related.forEach(item => {
                if (item.get('keywords').contains(keyword.toLowerCase())) {
                    hasRelated = true;
                }
            });
            if (!hasRelated) {
                return;
            }

            const parts = text.toLowerCase().split(keyword);
            let offset = 0;

            parts.forEach((part, i) => {
                if (i !== 0) {
                    decorations.push({
                        anchor: {key, offset: offset - keyword.length},
                        focus: {key, offset},
                        mark: {type: 'suggestion'},
                    });
                }

                offset = offset + part.length + keyword.length;
            });
        });
    });

    // Make the change to decorations without saving it into the undo history,
    // so that there isn't a confusing behavior when undoing.
    editor.withoutSaving(() => {
        editor.setDecorations(decorations);
    });
}

class SuggestionModal extends React.Component {

    constructor(props) {
        super(props);

        this.state = {
            url: ''
        };
    }

    closeModal() {
        this.props.editor.setState({
            'mySuggestionModalOpen': false,
            'mySuggestionKeyword': null,
        });
        this.setState({
            'keyword': null
        });
    }

    handleCancel(e) {
        this.closeModal.call(this);
    }

    handlePreview(item, e) {
        const url = '/content/preview/' + item.get('uuid');

        window.open(url, 'preview');
    }

    componentWillReceiveProps(newProps) {
        if (newProps.editor && newProps.editor.state.mySuggestionKeyword) {
            this.setState({
                'keyword': newProps.editor.state.mySuggestionKeyword
            });
        }
    }

    handleSave(item, e) {

        const keyword = this.state.keyword.props.children;
        const firstThree = keyword.substr(0, 3);
        const lastThree = keyword.substr(-3);

        let iterations = 0;

        // insert a character, then remove it to unset the decoration
        // because they don't really exist as selectable marks in the editor
        // model
        this.props.editor
            .focus()
            .insertText(' ')
            .deleteBackward(1);


        // start expanding the selection backward
        let go = true;
        while (go) {
            iterations++;
            if (iterations == 100) {
                return;
            }
            let text = this.props.editor.value.fragment.text;
            this.props.editor
                .focus()
                .moveAnchorBackward();

            if (text == keyword) {
                break;
            }

            if (text.indexOf(firstThree) === 0) {
                go = false;
            }
        }

        // start expanding it forward
        iterations = 0;
        go = true;
        while (go) {
            iterations++;
            if (iterations == 100) {
                return;
            }
            let text = this.props.editor.value.fragment.text;
            this.props.editor
                .focus()
                .moveFocusForward();

            if (text == keyword) {
                break;
            }

            if (text.substr(-3).indexOf(lastThree) === 0) {
                go = false;
            }
        }

        // once the selection is full, it has the one too many characters selected
        // so we contract it back down and insert the anchor.
        if (this.props.editor.value.fragment.text.indexOf(keyword) !== 0) {
            this.props.editor
                .focus()
                .moveAnchorForward();
        }
        if (this.props.editor.value.fragment.text.length !== keyword.length) {
            this.props.editor
                .focus()
                .moveFocusBackward();
        }

        this.props.editor
            .focus()
            .wrapInline({
                type: 'anchor',
                data: {
                    'href': '/' + item.get('uuid'),
                    'target': ''
                }
            });

        setTimeout(() => {
            this.closeModal.call(this);
        }, 250);

        return;

    }

    render() {
        const buttons = [
            <FlatButton
                label='Cancel'
                onClick={this.handleCancel.bind(this)}
                />,
        ];

        let suggestions = Immutable.fromJS([]);
        if (this.state.keyword) {
            const keyword = this.state.keyword.props.children.toLowerCase();

            suggestions = related.filter(item => {
                return item.get('keywords').indexOf(keyword) !== -1;
            });
        }

        return (
            <Dialog
                modal={true}
                actions={buttons}
                title='Insert Suggested Link'
                open={this.props.editor.state.mySuggestionModalOpen ? true : false}
                >
                {suggestions.map((suggestion, i) => (
                    <div key={i} className='clear-bottom'>
                        <span onClick={this.handleSave.bind(this, suggestion)} className='faux-link'>{suggestion.get('title')}</span><br />
                        {suggestion.get('published_at')} | <FlatButton primary={true} onClick={this.handlePreview.bind(this, suggestion)} label="Preview Article" />
                    </div>
                ))}
            </Dialog>
        );
    }
};

class SuggestionNote extends React.Component {
    render() {
        if (this.props.open) {
            return (
                <div style={{backgroundColor:'#ffc', margin: '5px 0', padding: '2px 5px', fontSize: '0.8em'}}>Select a keyword to insert a link. Click the "Suggest Links" button again to hide suggested links.</div>
            );
        } else {
            return '';
        }
    }
};

function SuggestionPlugin(options) {
    keywords = options.keywords || Immutable.fromJS([]);
    related = options.related || Immutable.fromJS([]);

    return {
        renderEditor(props, editor, next) {
            const children = next();
            return (
                <React.Fragment>
                    <SuggestionNote open={isActive} />
                    {children}
                    <SuggestionModal editor={editor} />
                </React.Fragment>
            );
        },
        renderMark(props, editor, next) {
            const {children, mark, attributes} = props;
            switch (mark.type) {
                case 'suggestion':
                    return <SuggestionNode {...props} editor={editor}/>;
                default:
                    return next();
            }
        }
    };
};
export {SuggestionPlugin, suggestionClickHandler, SuggestionButton};