Home Reference Source

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

import React from 'react';
import ToolbarButton from '../common/toolbar-button';
import Avatar from 'material-ui/Avatar';
import Chip from 'material-ui/Chip';
import FlatButton from 'material-ui/FlatButton';
import uuid from 'uuid';
import {findDOMNode} from 'slate-react';
import {connect} from 'react-redux';
import {noteCommentSetSelected, noteCommentsFetch, noteCommentCreate} from '../../../../redux/actions/note-comment-actions';
import SimpleEditor from '../../simple-editor';
import {stateToHTML} from 'draft-js-export-html';
import LoadingIndicator from '../../loading-indicator';
import {timeToFormat} from '../../../../util/strings';

export const SERIALIZER_RULES = [
    {
        serialize(obj, children) {
            if (obj.object == 'inline' && obj.type == 'editorial-note') {
                // this should only return the children
                return <React.Fragment>{children}</React.Fragment>;
            }
        }
    }
];

export function handleNewNote(e) {
    const {editor} = this;
    const {editorContent} = this.state;
    const hasNote = this.hasInline('editorial-note');

    if (!hasNote) {
        const id = uuid.v4();
        editor.wrapInline({
            type: 'editorial-note',
            data: {
                uuid: id
            }
        });
    } else {
        editor.unwrapInline('editorial-note');
    }
}

export class NoteButton extends ToolbarButton
{
    handleOnClick(e) {
        return handleNewNote.call(this.props.editor, e);
    }

    getLabel() {
        return <i className='fa fa-comment-o'></i>;
    }
}

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

        this.parentEditor = this.props.editor;
    }

    handleClick(e) {
        const {data} = this.props.node;
        const id = data.get('uuid');
        if (this.props.editor.props.readOnly) {
            window.Store.dispatch(noteCommentSetSelected(id));
        }
    }

    render() {
        const {data} = this.props.node;
        const id = data.get('uuid');

        return (
            <span {...this.props.attributes} className='note-node' onClick={this.handleClick.bind(this)}>
                {this.props.children}
            </span>
        );
    }
}

export function NotePlugin(options) {
    return {
        renderEditor(props, editor, next) {
            const children = next();
            if (editor.value.inlines.some(inline => inline.type == 'editorial-note')) {
                const node =  editor.value.document.getClosestInline(editor.value.selection.start.key);
                if (node) {
                    const uuid = node.data.get('uuid');
                    window.Store.dispatch(noteCommentSetSelected(uuid));
                } else {
                    window.Store.dispatch(noteCommentSetSelected(null));
                }
            } else {
                window.Store.dispatch(noteCommentSetSelected(null));
            }
            return (
                <React.Fragment>
                    {children}
                    <WrappedNoteViewer editor={editor} />
                </React.Fragment>
            );
        },
        renderNode(props, editor, next) {
            const {attributes, children} = props;

            switch(props.node.type) {
                case 'editorial-note':
                    return <NoteNode {...props} editor={editor}/>;
                default:
                    return next();
            }

        }
    }
}

class NoteViewer extends React.Component
{

    render() {
        const notes = this.props.editor.value.document.filterDescendants((node) => {
            return node.type == 'editorial-note';
        });

        return (
            <div className="slate-editor-note-container">
                {notes.map((note, i) => {
                    return (
                        <WrappedNoteContainer key={i} note={note} editor={this.props.editor} />
                    )
                })}
            </div>
        )
    }
}
const WrappedNoteViewer = connect((state) => {
    return {
        noteComments: state.noteComments
    };
})(NoteViewer);

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

        this.state = {
            collapsed: true,
            parentNode: false,
            domNode: false,
            offsetTop: 0,
            offsetLeft: -100,
        };
    }

    componentWillMount() {
        const {dispatch} = this.props;
        dispatch(noteCommentsFetch({'root':this.props.note.data.get('uuid')}));
    }

    static getDerivedStateFromProps(nextProps, prevState) {
        let domNode;
        try {
            domNode = findDOMNode(nextProps.note);
        } catch (err) {
            try {
                domNode = findDOMNode(nextProps.editor.value.document.getClosestBlock(nextProps.note.key));
            } catch (err) {
                return prevState;
            }
        }

        let nextState = {};
        let parent = domNode;

        if (!prevState.parentNode) {
            for(;;) {
                if (parent.className.indexOf('editor-container') === -1) {
                    parent = parent.parentElement;
                    continue;
                }
                break;
            }

            nextState.parentNode = parent;
        } else {
            parent = prevState.parentNode;
        }

        const offsetTop = domNode.getBoundingClientRect().top + window.scrollY - 50;
        const offsetLeft = parent.getBoundingClientRect().width;

        nextState.offsetTop = offsetTop;
        nextState.offsetLeft = offsetLeft;

        return Object.assign({}, prevState, nextState);
    }

    render() {

        const styles = {
            position: 'absolute',
            top: this.state.offsetTop + 'px',
            left: this.state.offsetLeft + 'px',
            zIndex: 999
        };

        if (this.props.noteComments.selected != this.props.note.data.get('uuid')) {
            return (
                ''
            );
        } else {
            return (
                <WrappedNoteItem className='floating-note-container' style={styles} note={this.props.note} editor={this.props.editor} />
            );
        }
    }
}
const WrappedNoteContainer = connect((state) => {
    return {
        noteComments: state.noteComments
    };
})(NoteContainer);

class NoteItem extends React.Component
{
    constructor(props) {
        super(props)
    }

    componentWillMount() {
        const {dispatch} = this.props;
        dispatch(noteCommentsFetch(this.props.note.data.get('uuid')));
    }

    handleEditorChange(raw, state, name) {
        this.setState({noteContent: stateToHTML(state.getCurrentContent())});
    }

    saveReply(e) {
        const {dispatch} = this.props;

        const data = {
            'contentUuid': this.props.editor.props.contentUuid,
            'comment': this.state.noteContent,
            'version': this.props.editor.props.contentVersion
        };
        const ref = this.props.note.data.get('uuid');

        dispatch(noteCommentCreate(ref, data))
            .then(() => dispatch(noteCommentsFetch(ref)));
    }

    handleResolve(e) {
        if (!confirm('Are you sure you want to resolve this note?')) {
            return;
        }
        this.props.editor.moveToRangeOfNode(this.props.note)
            .unwrapInline('editorial-note');
    }

    render() {
        const style = Object.assign({}, this.props.style, {
            'zIndex': 9999,
        });

        if (this.props.noteComments.isFetching) {
            return (
                <div style={style} className='note-item'>
                    <LoadingIndicator />
                </div>
            );
        }

        const text = this.props.note.text;

        const daynjeryose = function(item) {
            return {
                __html: item.get('comment')
            };
        };

        return (
            <div style={style} className='note-item'>
                <span className='resolve-button' onClick={this.handleResolve.bind(this)} title="Resolve Comment"><i className='fa fa-check'></i></span>
                <span className='note-node'>"{text}"</span>
                {
                    this.props.noteComments.items.size
                    ? (
                        <React.Fragment>
                            {this.props.noteComments.items.toSeq().map((item) =>
                                <div key={item.get('uuid')} className='note-reply'>
                                    <Chip>
                                        <Avatar
                                            src={item.get('user').get('gravatar')}
                                            />
                                            {item.get('user').get('name')} said:
                                    </Chip>
                                    <div dangerouslySetInnerHTML={daynjeryose(item)}></div>
                                    <span className='date'> at {timeToFormat(item.get('created_at'), 'hh:mma, M/DD')}</span>
                                </div>
                            )}
                        </React.Fragment>
                    )
                    : ''
                }
                <SimpleEditor ref='simpleEditor' showToolbar={false} value='' onChange={this.handleEditorChange.bind(this)} style={{'fontSize': '12px'}}/>
                <FlatButton onClick={this.saveReply.bind(this)} label='Add Comment' />
            </div>
        );
    }
}

const WrappedNoteItem = connect((state) => {
    return {
        noteComments: state.noteComments
    };
})(NoteItem);