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