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};