Home Reference Source

application/components/common/rich-editor/media.js

import React from 'react';
import Immutable from 'immutable';
import {browserHistory, Link} from 'react-router';

import {AtomicBlockUtils, Entity} from 'draft-js';
import uuid from 'uuid';
import Dialog from 'material-ui/Dialog';
import TextField from 'material-ui/TextField';
import FlatButton from 'material-ui/FlatButton';

import ContentFlux, {ContentService} from './../../../services/content-service';
import Events from './../../../util/events';

import RichEditorMediaResizer from './media-resize';
import RichEditorMediaSearch from './media-search';

import LoadingIndicator from './../loading-indicator';

/**
 * Handles display of media search box, injecting media into editor and injecting
 * resize/align box.
 *
 * @author Mike Joseph <mike@getsnworks.com>
 */
class Media extends React.Component {

    constructor(props) {
        super(props);
        this.displayName = 'Media';

        // We're using a singleton so each media
        // block can manage its own files
        this.service = new ContentFlux();

        this.getValue = this.getValue.bind(this);
        this.handleCancel = this.handleCancel.bind(this);
        this.handleSelectMedia = this.handleSelectMedia.bind(this);
        this.setAlignment = this.setAlignment.bind(this);
        this.startPropertyEdit = this.startPropertyEdit.bind(this);
        this.injectSearchModal = this.injectSearchModal.bind(this);

        this.handleEditFinish = this.handleEditFinish.bind(this);

        this.onChange = this.onChange.bind(this);

        this.startEdit = this.startEdit.bind(this);
        this.finishEdit = this.finishEdit.bind(this);

        this.updateEntity = this.updateEntity.bind(this);

        this.state = this.service.stores.contents.getState();
        this.state.editMode = false;
        this.state.searchMode = false;
        this.state.mediaUuid = '';
        this.state.align = '';
        this.state.className = '';
        this.state.caption = '';
        this.state.credit = '';
        this.state.linkTo = '';
        this.state.width = 0;
        this.state.height = 0;
    }

    /**
     * Attach to main store events and set the initial mode
     */
    componentWillMount() {

        this.service.stores.contents.listen(this.onChange);

        this.setState(this.getValue());

        if (!this.getValue().mediaUuid && !this.getValue().origSrc) {
            this.setState({'searchMode': true});
            this.startEdit();
            // this.injectSearchModal();
        } else if(this.getValue().mediaUuid) {
            this.service.actions.contents.fetchOne(this.getValue().mediaUuid);
        } else {
            // it's just an embedded url, not a tracked media file
            this.setState({'content': Immutable.fromJS({
                attachment: {
                    public_url: this.getValue().origSrc
                }
            })});
        }
    }

    componentWillUnmount() {
        this.service.stores.contents.unlisten(this.onChange);
    }

    onChange(state) {
        this.setState(state);
    }

    /**
     * Inject the search modal into the root space (so it layers properly)
     */
    injectSearchModal() {
        const id = uuid.v4();
        const actions = [
            <FlatButton
                onClick={this.handleCancel.bind(this, id)}
                label='Cancel'
                />
        ];

        const dialog = (
            <Dialog
                actions={actions}
                modal={false}
                open={true}
                autoScrollBodyContent={true}
                repositionOnUpdate={true}
                style={{top:0}}
                >

                <RichEditorMediaSearch onSelectUrl={this.handleSelectUrl.bind(this, id)} onSelectMedia={this.handleSelectMedia.bind(this, id)} />
            </Dialog>
        );
        // Events.emit('addRootComponent', id, dialog);
        return dialog;
    }

    /**
     * Returns meta data as object
     * @return {Object} meta data
     */
    getValue() {
        return this.props.contentState
            .getEntity(this.props.block.getEntityAt(0))
            .getData();
    }

    /**
     * Update metadata. Currently tracks
     * <ul>
     *     <li>height</li>
     *     <li>width</li>
     *     <li>align</li>
     *     <li>mediaUuid</li>
     *     <li>origSrc</li>
     * </ul>
     */
    updateEntity() {
        const {height, width, align, mediaUuid, origSrc, linkTo} = this.state;

        const key = this.props.block.getEntityAt(0);
        this.props.contentState.mergeEntityData(key, {
            height: height,
            width: width,
            align: align,
            mediaUuid: mediaUuid,
            origSrc: origSrc,
            linkTo: linkTo
        });
    }

    /**
     * Callback to retain lock on editor
     */
    startEdit() {
        this.props.blockProps.onStartEdit(this.props.block.getKey());
    }

    /**
     * Callback to release editor lock, but also save meta properties
     */
    finishEdit() {
        this.updateEntity();
        this.props.blockProps.onFinishEdit(this.props.block.getKey());
    }

    onCancelEdit() {
        this.updateEntity();
        this.props.blockProps.onCancelEdit(this.props.block.getKey());
    }

    /**
     * Cancel the search modal. Because this is injected into the root
     * we need to track by id.
     * @param  {string} id Component id
     * @param  {event} e   Click event
     */
    handleCancel(id, e) {
        // search mode
        Events.emit('removeRootComponent', id);
        this.setState({editMode: false, searchMode: false}, () => { this.onCancelEdit() });
    }

    /**
     * Callback that bubbles up through the child components when
     * a media file is selected
     * @param  {string} id    Modal component id
     * @param  {Map} media Media
     */
    handleSelectMedia(id, media) {
        Events.emit('removeRootComponent', id);
        this.service.actions.contents.fetchOne(media.get('uuid')).then(() => {
            this.setState({
                searchMode: false,
                editMode: true,
                mediaUuid: this.state.content.get('uuid'),
                origSrc: this.state.content.get('attachment').get('public_url')
            }, () => { this.finishEdit() });
        });
    }

    /**
     * Callback that bubbles up through the child components when
     * a media url is entered
     * @param {string} id Modal component id
     * @param {Map} media Media
     */
    handleSelectUrl(id, media) {
        Events.emit('removeRootComponent', id);

        this.setState({
            searchMode: false,
            editMode: true,
            mediaUuid: false,
            origSrc: media.url,
            linkTo: false,
            content: Immutable.fromJS({
                attachment: {
                    public_url: media.url
                }
            })
        }, () => { this.finishEdit() });
    }

    /**
     * Handle the end state on resize and align
     * @param  {object} data Mostly state data
     */
    handleEditFinish(data) {
        const {width, height} = data;

        this.setState({
            width: width,
            height: height,

            editMode: false,
            searchMode: false
        }, () => { this.finishEdit() });
    }

    /**
     * Handle resize start event
     */
    startPropertyEdit() {
        this.setState({editMode: true, searchMode: false}, () => { this.startEdit(); });
    }

    /**
     * Simply set the media alignment
     * @param {string} alignment
     * @param {event} e
     */
    setAlignment(alignment, e) {
        this.setState({align: alignment}, () => { this.updateEntity() });
    }

    handleMediaView(e) {
        if (this.state.content.get('uuid')) {
            let linkLocation = '/ceo/locate/' + this.state.content.get('uuid');
            browserHistory.push(linkLocation);
            return;
        }
        window.open(this.state.content.get('attachment').get('public_url'));
    }

    handleMediaLink(e) {
        const url = prompt('Please enter a full url (Don\'t forget the http://)', this.state.linkTo ? this.state.linkTo : 'http://');
        this.setState({linkTo: url}, () => {this.updateEntity()});
    }

    render() {

        if (this.state.searchMode || this.state.editMode) {
            if (this.state.searchMode) {

                // this is actually loaded in the componentWillMount method
                // to avoid changing state during render. it works there since
                // the search box is only created on initial mount
                return this.injectSearchModal();

            } else if (this.state.editMode) {
                return (
                    <RichEditorMediaResizer content={this.state.content} width={this.state.width} height={this.state.height} align={this.state.align} onFinish={this.handleEditFinish} />
                );
            }
        }

        // this is generally only of they canceled the search and there's no
        // media to load or display
        if (!this.state.mediaUuid && !this.state.origSrc) {
            return <span />;
        }

        if (!this.state.content.size) {
            return <LoadingIndicator size="small" />;
        }

        const className = 'alignment-container ' + (this.state.align ? this.state.align : 'center' );

        const height = this.state.height ? this.state.height : '100%';
        const width = this.state.width ? this.state.width : '100%';

        // this lovely mess of divs allows the editor to display the media with wrapped text, as necessary
        // but still allow overlay buttons to be clickable. otherwise the text blocks overlay the floated media
        // making it unclickable
        return (
            <div className={className} style={{height: height, width: width}}>
                <div className='alignment-inner' style={{height: height}}>
                    <div className='alignment-content' style={{height: height}}>
                        <div className='top-toolbar'>
                            <i className='fa fa-align-left' title='Left align' onClick={this.setAlignment.bind(this, 'left')}></i>
                            <i className='fa fa-align-center' title='Center align' onClick={this.setAlignment.bind(this, 'center')}></i>
                            <i className='fa fa-align-right' title='Right align' onClick={this.setAlignment.bind(this, 'right')}></i>
                            <i className='fa fa-arrows-alt' title='Resize' onClick={this.startPropertyEdit}></i>
                            <i className='fa fa-link' title='View' onClick={this.handleMediaLink.bind(this)}></i>
                            <i className='fa fa-edit' title='View' onClick={this.handleMediaView.bind(this)}></i>
                        </div>
                        <img src={this.state.content.get('attachment').get('public_url')} style={{height: height, width: width, maxWidth:'100%'}} onClick={() => {console.log('hi5');}} />
                    </div>
                </div>
            </div>
        );
    }
}

const insertMedia = (editorState) => {
    const newContentState = editorState.getCurrentContent().createEntity(
        'TOKEN',
        'IMMUTABLE',
        {customType: 'media', content: '',}
    );
    const entityKey = newContentState.getLastCreatedEntityKey();

    return AtomicBlockUtils.insertAtomicBlock(
        editorState,
        entityKey,
        ' '
    );
}

export {Media as default, insertMedia};