Home Reference Source

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

  1. import React from 'react';
  2. import Immutable from 'immutable';
  3. import {browserHistory, Link} from 'react-router';
  4.  
  5. import {AtomicBlockUtils, Entity} from 'draft-js';
  6. import uuid from 'uuid';
  7. import Dialog from 'material-ui/Dialog';
  8. import TextField from 'material-ui/TextField';
  9. import FlatButton from 'material-ui/FlatButton';
  10.  
  11. import ContentFlux, {ContentService} from './../../../services/content-service';
  12. import Events from './../../../util/events';
  13.  
  14. import RichEditorMediaResizer from './media-resize';
  15. import RichEditorMediaSearch from './media-search';
  16.  
  17. import LoadingIndicator from './../loading-indicator';
  18.  
  19. /**
  20. * Handles display of media search box, injecting media into editor and injecting
  21. * resize/align box.
  22. *
  23. * @author Mike Joseph <mike@getsnworks.com>
  24. */
  25. class Media extends React.Component {
  26.  
  27. constructor(props) {
  28. super(props);
  29. this.displayName = 'Media';
  30.  
  31. // We're using a singleton so each media
  32. // block can manage its own files
  33. this.service = new ContentFlux();
  34.  
  35. this.getValue = this.getValue.bind(this);
  36. this.handleCancel = this.handleCancel.bind(this);
  37. this.handleSelectMedia = this.handleSelectMedia.bind(this);
  38. this.setAlignment = this.setAlignment.bind(this);
  39. this.startPropertyEdit = this.startPropertyEdit.bind(this);
  40. this.injectSearchModal = this.injectSearchModal.bind(this);
  41.  
  42. this.handleEditFinish = this.handleEditFinish.bind(this);
  43.  
  44. this.onChange = this.onChange.bind(this);
  45.  
  46. this.startEdit = this.startEdit.bind(this);
  47. this.finishEdit = this.finishEdit.bind(this);
  48.  
  49. this.updateEntity = this.updateEntity.bind(this);
  50.  
  51. this.state = this.service.stores.contents.getState();
  52. this.state.editMode = false;
  53. this.state.searchMode = false;
  54. this.state.mediaUuid = '';
  55. this.state.align = '';
  56. this.state.className = '';
  57. this.state.caption = '';
  58. this.state.credit = '';
  59. this.state.linkTo = '';
  60. this.state.width = 0;
  61. this.state.height = 0;
  62. }
  63.  
  64. /**
  65. * Attach to main store events and set the initial mode
  66. */
  67. componentWillMount() {
  68.  
  69. this.service.stores.contents.listen(this.onChange);
  70.  
  71. this.setState(this.getValue());
  72.  
  73. if (!this.getValue().mediaUuid && !this.getValue().origSrc) {
  74. this.setState({'searchMode': true});
  75. this.startEdit();
  76. // this.injectSearchModal();
  77. } else if(this.getValue().mediaUuid) {
  78. this.service.actions.contents.fetchOne(this.getValue().mediaUuid);
  79. } else {
  80. // it's just an embedded url, not a tracked media file
  81. this.setState({'content': Immutable.fromJS({
  82. attachment: {
  83. public_url: this.getValue().origSrc
  84. }
  85. })});
  86. }
  87. }
  88.  
  89. componentWillUnmount() {
  90. this.service.stores.contents.unlisten(this.onChange);
  91. }
  92.  
  93. onChange(state) {
  94. this.setState(state);
  95. }
  96.  
  97. /**
  98. * Inject the search modal into the root space (so it layers properly)
  99. */
  100. injectSearchModal() {
  101. const id = uuid.v4();
  102. const actions = [
  103. <FlatButton
  104. onClick={this.handleCancel.bind(this, id)}
  105. label='Cancel'
  106. />
  107. ];
  108.  
  109. const dialog = (
  110. <Dialog
  111. actions={actions}
  112. modal={false}
  113. open={true}
  114. autoScrollBodyContent={true}
  115. repositionOnUpdate={true}
  116. style={{top:0}}
  117. >
  118.  
  119. <RichEditorMediaSearch onSelectUrl={this.handleSelectUrl.bind(this, id)} onSelectMedia={this.handleSelectMedia.bind(this, id)} />
  120. </Dialog>
  121. );
  122. // Events.emit('addRootComponent', id, dialog);
  123. return dialog;
  124. }
  125.  
  126. /**
  127. * Returns meta data as object
  128. * @return {Object} meta data
  129. */
  130. getValue() {
  131. return this.props.contentState
  132. .getEntity(this.props.block.getEntityAt(0))
  133. .getData();
  134. }
  135.  
  136. /**
  137. * Update metadata. Currently tracks
  138. * <ul>
  139. * <li>height</li>
  140. * <li>width</li>
  141. * <li>align</li>
  142. * <li>mediaUuid</li>
  143. * <li>origSrc</li>
  144. * </ul>
  145. */
  146. updateEntity() {
  147. const {height, width, align, mediaUuid, origSrc, linkTo} = this.state;
  148.  
  149. const key = this.props.block.getEntityAt(0);
  150. this.props.contentState.mergeEntityData(key, {
  151. height: height,
  152. width: width,
  153. align: align,
  154. mediaUuid: mediaUuid,
  155. origSrc: origSrc,
  156. linkTo: linkTo
  157. });
  158. }
  159.  
  160. /**
  161. * Callback to retain lock on editor
  162. */
  163. startEdit() {
  164. this.props.blockProps.onStartEdit(this.props.block.getKey());
  165. }
  166.  
  167. /**
  168. * Callback to release editor lock, but also save meta properties
  169. */
  170. finishEdit() {
  171. this.updateEntity();
  172. this.props.blockProps.onFinishEdit(this.props.block.getKey());
  173. }
  174.  
  175. onCancelEdit() {
  176. this.updateEntity();
  177. this.props.blockProps.onCancelEdit(this.props.block.getKey());
  178. }
  179.  
  180. /**
  181. * Cancel the search modal. Because this is injected into the root
  182. * we need to track by id.
  183. * @param {string} id Component id
  184. * @param {event} e Click event
  185. */
  186. handleCancel(id, e) {
  187. // search mode
  188. Events.emit('removeRootComponent', id);
  189. this.setState({editMode: false, searchMode: false}, () => { this.onCancelEdit() });
  190. }
  191.  
  192. /**
  193. * Callback that bubbles up through the child components when
  194. * a media file is selected
  195. * @param {string} id Modal component id
  196. * @param {Map} media Media
  197. */
  198. handleSelectMedia(id, media) {
  199. Events.emit('removeRootComponent', id);
  200. this.service.actions.contents.fetchOne(media.get('uuid')).then(() => {
  201. this.setState({
  202. searchMode: false,
  203. editMode: true,
  204. mediaUuid: this.state.content.get('uuid'),
  205. origSrc: this.state.content.get('attachment').get('public_url')
  206. }, () => { this.finishEdit() });
  207. });
  208. }
  209.  
  210. /**
  211. * Callback that bubbles up through the child components when
  212. * a media url is entered
  213. * @param {string} id Modal component id
  214. * @param {Map} media Media
  215. */
  216. handleSelectUrl(id, media) {
  217. Events.emit('removeRootComponent', id);
  218.  
  219. this.setState({
  220. searchMode: false,
  221. editMode: true,
  222. mediaUuid: false,
  223. origSrc: media.url,
  224. linkTo: false,
  225. content: Immutable.fromJS({
  226. attachment: {
  227. public_url: media.url
  228. }
  229. })
  230. }, () => { this.finishEdit() });
  231. }
  232.  
  233. /**
  234. * Handle the end state on resize and align
  235. * @param {object} data Mostly state data
  236. */
  237. handleEditFinish(data) {
  238. const {width, height} = data;
  239.  
  240. this.setState({
  241. width: width,
  242. height: height,
  243.  
  244. editMode: false,
  245. searchMode: false
  246. }, () => { this.finishEdit() });
  247. }
  248.  
  249. /**
  250. * Handle resize start event
  251. */
  252. startPropertyEdit() {
  253. this.setState({editMode: true, searchMode: false}, () => { this.startEdit(); });
  254. }
  255.  
  256. /**
  257. * Simply set the media alignment
  258. * @param {string} alignment
  259. * @param {event} e
  260. */
  261. setAlignment(alignment, e) {
  262. this.setState({align: alignment}, () => { this.updateEntity() });
  263. }
  264.  
  265. handleMediaView(e) {
  266. if (this.state.content.get('uuid')) {
  267. let linkLocation = '/ceo/locate/' + this.state.content.get('uuid');
  268. browserHistory.push(linkLocation);
  269. return;
  270. }
  271. window.open(this.state.content.get('attachment').get('public_url'));
  272. }
  273.  
  274. handleMediaLink(e) {
  275. const url = prompt('Please enter a full url (Don\'t forget the http://)', this.state.linkTo ? this.state.linkTo : 'http://');
  276. this.setState({linkTo: url}, () => {this.updateEntity()});
  277. }
  278.  
  279. render() {
  280.  
  281. if (this.state.searchMode || this.state.editMode) {
  282. if (this.state.searchMode) {
  283.  
  284. // this is actually loaded in the componentWillMount method
  285. // to avoid changing state during render. it works there since
  286. // the search box is only created on initial mount
  287. return this.injectSearchModal();
  288.  
  289. } else if (this.state.editMode) {
  290. return (
  291. <RichEditorMediaResizer content={this.state.content} width={this.state.width} height={this.state.height} align={this.state.align} onFinish={this.handleEditFinish} />
  292. );
  293. }
  294. }
  295.  
  296. // this is generally only of they canceled the search and there's no
  297. // media to load or display
  298. if (!this.state.mediaUuid && !this.state.origSrc) {
  299. return <span />;
  300. }
  301.  
  302. if (!this.state.content.size) {
  303. return <LoadingIndicator size="small" />;
  304. }
  305.  
  306. const className = 'alignment-container ' + (this.state.align ? this.state.align : 'center' );
  307.  
  308. const height = this.state.height ? this.state.height : '100%';
  309. const width = this.state.width ? this.state.width : '100%';
  310.  
  311. // this lovely mess of divs allows the editor to display the media with wrapped text, as necessary
  312. // but still allow overlay buttons to be clickable. otherwise the text blocks overlay the floated media
  313. // making it unclickable
  314. return (
  315. <div className={className} style={{height: height, width: width}}>
  316. <div className='alignment-inner' style={{height: height}}>
  317. <div className='alignment-content' style={{height: height}}>
  318. <div className='top-toolbar'>
  319. <i className='fa fa-align-left' title='Left align' onClick={this.setAlignment.bind(this, 'left')}></i>
  320. <i className='fa fa-align-center' title='Center align' onClick={this.setAlignment.bind(this, 'center')}></i>
  321. <i className='fa fa-align-right' title='Right align' onClick={this.setAlignment.bind(this, 'right')}></i>
  322. <i className='fa fa-arrows-alt' title='Resize' onClick={this.startPropertyEdit}></i>
  323. <i className='fa fa-link' title='View' onClick={this.handleMediaLink.bind(this)}></i>
  324. <i className='fa fa-edit' title='View' onClick={this.handleMediaView.bind(this)}></i>
  325. </div>
  326. <img src={this.state.content.get('attachment').get('public_url')} style={{height: height, width: width, maxWidth:'100%'}} onClick={() => {console.log('hi5');}} />
  327. </div>
  328. </div>
  329. </div>
  330. );
  331. }
  332. }
  333.  
  334. const insertMedia = (editorState) => {
  335. const newContentState = editorState.getCurrentContent().createEntity(
  336. 'TOKEN',
  337. 'IMMUTABLE',
  338. {customType: 'media', content: '',}
  339. );
  340. const entityKey = newContentState.getLastCreatedEntityKey();
  341.  
  342. return AtomicBlockUtils.insertAtomicBlock(
  343. editorState,
  344. entityKey,
  345. ' '
  346. );
  347. }
  348.  
  349. export {Media as default, insertMedia};