import React from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import { RichUtils, Editor, Modifier, EditorState } from 'draft-js' import clamp from 'lodash/clamp'; import upperFirst from 'lodash/upperFirst'; import CustomStyleManager from './custom-style-manager/custom-style-manager' import EditorStateUtil from './util/editor-state-util' import StyleButton from './toolbar/style-button' import StyleOptionsMenu from './toolbar/style-options-menu' import ColorSelectionButton from './toolbar/color-selection-button' import LinkButton from './toolbar/link-button' import ImageButton from './toolbar/image-button' import Counter from './toolbar/counter' import createDecorators from './custom-components/decorators' import { fetchEntity, updateEntity } from '../../../actions/index' interface TextEditorProps { blogPost: any; blogPostId: number contentState: string fetchEntity: any; editorIsSaved: boolean onMouseDown?: any setEditorSavedStatus: any updateBlogPost: any updateEntity: any } interface TextEditorState extends ComponentState { editorState: any pseudoStyles: any[] readOnly: boolean } class TextEditor extends React.Component { decorators: any editor: any setDomEditorRef: any styles: any constructor(props) { super(props); this.styles = new CustomStyleManager(); this.bindFns(); this.decorators = createDecorators(this.getEditorState, this.updateEditorState, this.getEntityDataByKey, this.mergeEntityData, this.setReadOnly); this.state = { readOnly: false, pseudoStyles: [], editorState: EditorStateUtil.loadEditorState("", this.decorators) }; this.setDomEditorRef = (ref) => this.editor = ref; // Used to manage focus setTimeout(this._handleFocusEditor.bind(this), 0); }; getEditorState() { return this.state.editorState; } setReadOnly(value) { this.setState({ readOnly: value }); } bindFns() { this.setReadOnly = this.setReadOnly.bind(this); this.getEntityDataByKey = this.getEntityDataByKey.bind(this); this.getEditorState = this.getEditorState.bind(this); this.updateEditorState = this.updateEditorState.bind(this); this.mergeEntityData = this.mergeEntityData.bind(this); } updateEditorState(newEditorState, bypassReadOnly = false) { if (this.state.readOnly && bypassReadOnly) { this.setState({ readOnly: false }, () => { this.setState({ editorState: newEditorState, readOnly: true, }); }); return; } if (!newEditorState.getCurrentInlineStyle().size) { const defaultBlockInline = this.styles.defaultBlockInline; const defaultStyles = defaultBlockInline[this.currentBlock().getType()] || defaultBlockInline.unstyled; defaultStyles.forEach(style => { newEditorState = EditorStateUtil.toggleInlineStyle(newEditorState, style) }); } this.setState({ editorState: newEditorState }, () => { const { editorState } = this.state; let { blogPost } = this.props; blogPost.editorContentState = EditorStateUtil.rawContentString(editorState); blogPost.htmlContent = this.styles.exportHTML(editorState); this.props.updateBlogPost(blogPost); }); } loadEditorState(contentState) { return EditorStateUtil.loadEditorState(contentState, this.decorators); } // LIFECYCLE METHODS ----- componentDidMount() { let contentState = this.props.blogPost.editorContentState; this.setState({ editorState: this.loadEditorState(contentState) }); } // EDITOR STATE RETRIEVAL ----- currentInlineStylesInclude(styleCode) { return this.state.editorState.getCurrentInlineStyle().has(styleCode); } currentColor() { return EditorStateUtil.currentColor(this.state.editorState); } currentBlock() { return EditorStateUtil.currentBlock(this.state.editorState); } previousBlock() { return EditorStateUtil.previousBlock(this.state.editorState); } currentBlockType(blockType) { return this.currentBlock().getType(); } currentBlockData(key) { return this.currentBlock().getData().get(key); } currentEntityType() { return EditorStateUtil.currentEntityType(this.state.editorState); } getEntityDataByKey(editorState, entityKey) { return EditorStateUtil.getEntityDataByKey(editorState, entityKey); } // EDITOR STATE STATE MANIPULATION ----- toggleInlineStyle(styleCode) { const newEditorState = EditorStateUtil.toggleInlineStyle(this.state.editorState, styleCode); this.updateEditorState(newEditorState); } toggleColorStyle(styleCode) { const newEditorState = EditorStateUtil.toggleColorStyle(this.state.editorState, styleCode); this.updateEditorState(newEditorState); } toggleBlockType(styleCode) { const newEditorState = EditorStateUtil.toggleBlockType(this.state.editorState, styleCode); this.updateEditorState(newEditorState); } toggleBlockMetaStyle(styleCode) { this.setState({ readOnly: false }, () => { const newEditorState = EditorStateUtil.toggleBlockMetaStyle(this.state.editorState, styleCode); this.updateEditorState(newEditorState, true); }); } mergeBlockData(data) { const newEditorState = EditorStateUtil.mergeBlockData(this.state.editorState, data); this.updateEditorState(newEditorState); } mergeBlockDataByKey(blockKey, data) { const newEditorState = EditorStateUtil.mergeBlockDataByKey(this.state.editorState, blockKey, data); this.updateEditorState(newEditorState); } applyEntity(entityType, mutability, data = {}) { const { editorState } = this.state; let newEditorState; switch(entityType) { case 'LINK': newEditorState = EditorStateUtil.applyEntity(editorState, 'LINK', 'MUTABLE', data); break; case 'IMAGE': case 'CTA': case 'HTML': case 'TESTIMONIAL': case 'TIP': newEditorState = EditorStateUtil.applyAtomicBlock(editorState, entityType, 'IMMUTABLE', data); break; }; this.updateEditorState(newEditorState); } removeEntity() { const newEditorState = EditorStateUtil.removeEntity(this.state.editorState); this.updateEditorState(newEditorState); } mergeEntityData(entityKey, data) { const newEditorState = EditorStateUtil.mergeEntityData(this.state.editorState, entityKey, data); this.updateEditorState(newEditorState); } selectAtomicBlock(blockKey) { this.setState({ readOnly: true }, () => { const newEditorState = EditorStateUtil.selectAtomicBlock(this.state.editorState, blockKey); this.updateEditorState(newEditorState); }); } removeAtomicBlock(blockKey) { this.setState({ readOnly: false }, () => { const newEditorState = EditorStateUtil.removeAtomicBlock(this.state.editorState, blockKey); this.updateEditorState(newEditorState, true); }); } applyPseudoStyle(styleType) { const { editorState, pseudoStyles } = this.state; const newEditorState = EditorStateUtil.applyPseudoStyle(editorState, styleType); const newPseudoStyle = { selectionState: editorState.getSelection(), style: `PSEUDO_${upperFirst(styleType)}`, }; pseudoStyles.push(newPseudoStyle); this.setState({ pseudoStyles }); this.updateEditorState(newEditorState); } // RENDERING ----- render() { return(
{ this.renderToolbar() } { this.renderEditor() }
); } renderEditor() { const { editorState, readOnly } = this.state; const contentState = editorState.getCurrentContent(); return (
this.styles.blockStyleFn(block, contentState) } blockRendererFn={ (block) => this.styles.blockRendererFn(block, contentState, this._customBlockProps()) } customStyleMap={ this.styles.customStylesMap } customStyleFn={ this.styles.customStyleFn } />
); } renderToolbar() { return (
{ this.renderBlockHeaderButtons() } { this.renderInlineStyleButtons() } { this.renderBlockListButtons() } { this.renderMetaStyleButtons() } { this.renderEntityButtons() } { this.renderCustomBlockButtons() }
); } renderInlineStyleButtons() { return (
{ this.styles.inline.map(styleCode => this.renderStyleButton(styleCode, 'inline')) }
); } renderBlockListButtons() { const listStyles = this.styles.block.list; return (
{ listStyles.map(styleCode => this.renderStyleButton(styleCode, 'block')) }
); } renderBlockHeaderButtons() { const headerStyles = this.styles.block.header; return (
this.currentBlockType(type) === type)[0] } defaultOption="unstyled" menuDirection="vertical" renderStyleButton={ this.renderStyleButton.bind(this) } />
); } renderMetaStyleButtons() { const currentAlignDirection = this.currentBlockData('align'); return (
); } renderEntityButtons() { return (
this.applyPseudoStyle('SELECT') } />
); } renderCustomBlockButtons() { const entityTypes = this.styles.customEntity; return (
{ entityTypes.map(entityType => this.renderStyleButton(entityType, 'custom')) }
); } renderStyleButton(styleCode, type, options = {}) { const props = { key: styleCode, styleCode, options, }; switch(type) { case 'inline': props["onMouseDown"] = this.toggleInlineStyle.bind(this); props["isPressed"] = this.currentInlineStylesInclude(styleCode); break; case 'block': props["onMouseDown"] = this.toggleBlockType.bind(this); props["isPressed"] = this.currentBlockType(styleCode) === styleCode; break; case 'custom': props["onMouseDown"] = (styleCode) => this.applyEntity(styleCode, 'IMMUTABLE'); props["isPressed"] = this.currentEntityType() === styleCode; case 'meta': const [key, value] = styleCode.split('-'); switch(key) { case 'align': const alignDirection = this.currentBlockData('align') || 'left'; props["onMouseDown"] = this.toggleBlockMetaStyle.bind(this); props["isPressed"] = alignDirection === value; break; default: break; } break; } return } pasteHTML() { const rawHTML = this.refs.testHTML["value"]; const newEditorState = EditorStateUtil.stateFromHTML(rawHTML); this.updateEditorState(newEditorState); } renderDebugButtons() { const editorState = this.state.editorState; const selectionState = editorState.getSelection().toJS(); const contentState = editorState.getCurrentContent(); const currentBlock = this.currentBlock(); const currentEntityKey = currentBlock.getEntityAt(selectionState.anchorOffset); const currentEntity = currentEntityKey && contentState.getEntity(currentEntityKey).toJS(); return (
console.log(this.state) }>Parent State console.log(editorState.toJS()) }>Editor State console.log(contentState.toJS()) }>Content State console.log(this.currentBlock().toJS()) }>Curent Block console.log(currentEntity) }>Current Entity console.log(this.styles.exportHTML(editorState)) }>HTML Output Paste HTML
); } // PRIVATE ----- _removePseudoStyles() { const { editorState, pseudoStyles } = this.state; const currentSelection = editorState.getSelection(); let newEditorState = editorState; let newContentState = editorState.getCurrentContent(); if (!pseudoStyles.length) { return; } while (pseudoStyles.length) { const { style, selectionState } = pseudoStyles.pop(); newContentState = Modifier.removeInlineStyle( newEditorState.getCurrentContent(), selectionState, style ); } newEditorState = EditorState.push( newEditorState, newContentState, 'change-inline-style' ); newEditorState = EditorState.forceSelection(newEditorState, currentSelection); this.updateEditorState(newEditorState); } _customBlockProps() { return { selectAtomicBlock: this.selectAtomicBlock.bind(this), removeAtomicBlock: this.removeAtomicBlock.bind(this), currentAlignDirection: () => this.currentBlockData('align') || 'left', toggleBlockMetaStyle: this.toggleBlockMetaStyle.bind(this), mergeBlockDataByKey: this.mergeBlockDataByKey.bind(this), mergeEntityData: this.mergeEntityData.bind(this), renderStyleButton: this.renderStyleButton.bind(this), }; } // PRIVATE HANDLERS ----- _handleFocusEditor(e) { if (e) { let parentEl = e.target; while (parentEl && parentEl.id !== 'text-editor-admin') { if (parentEl.dataset.remainReadOnly === 'true') { return; } parentEl = parentEl.parentElement; } } this.editor.focus(); this._removePseudoStyles(); if (this.state.readOnly) { this.setState({ readOnly: false }); } } _handleKeyCommand(command) { const { editorState } = this.state; const handledEditorState = RichUtils.handleKeyCommand(editorState, command); if (handledEditorState) { this.updateEditorState(handledEditorState); return 'handled'; } let newEditorState = editorState; switch(command) { case 'split-block': const data = this.currentBlock().toJS().data; // Cache metadata to re-apply newEditorState = EditorStateUtil.splitBlock(newEditorState); newEditorState = EditorStateUtil.mergeBlockData(newEditorState, data); const blockType = EditorStateUtil.currentBlock(newEditorState).getType(); if (blockType !== 'unordered-list-item' && blockType !== 'ordered-list-item') { newEditorState = EditorStateUtil.mergeBlockData(newEditorState, { depth: 0 }); } this.updateEditorState(newEditorState); return 'handled'; } return 'not-handled'; } _handleTab(e) { e.preventDefault(); const { editorState } = this.state; const currentBlock = this.currentBlock(); const blockType = currentBlock.getType(); let newEditorState, depth; switch (blockType) { case 'ordered-list-item': case 'unordered-list-item': const [minDepth, maxDepth] = this._calculateDepthBounds(currentBlock, blockType); depth = this.currentBlockData('depth') || 0; depth += e.shiftKey ? -1 : 1; depth = clamp(depth, minDepth, maxDepth); newEditorState = EditorStateUtil.mergeBlockData(editorState, { depth }); this.updateEditorState(newEditorState); break; } } _handleBeforeInput(chars) { const { editorState } = this.state; switch(chars) { case ' ': const currentEntityType = EditorStateUtil.currentEntityType(editorState); const nextEntityType = EditorStateUtil.nextEntityType(editorState); if (currentEntityType === 'LINK' && nextEntityType !== 'LINK') { const newEditorState = EditorStateUtil.insertText(editorState, ' '); this.updateEditorState(newEditorState); return 'handled'; } default: return 'not-handled'; } } // PRIVATE HELPERS ----- _calculateDepthBounds(block, blockType) { let [minDepth, maxDepth] = [0, 4]; if (blockType === 'ordered-list-item') { const prevBlock = this.previousBlock(); if (prevBlock) { const prevBlockDepth = prevBlock.getData().get('depth') || 0; maxDepth = (prevBlockDepth + 1 > maxDepth ? maxDepth : prevBlockDepth + 1); } else { maxDepth = 0; } } return [minDepth, maxDepth]; } } const mapStateToProps = (reducers, props) => { return reducers.standardReducer; }; function mapDispatchToProps(dispatch) { return bindActionCreators({ fetchEntity, updateEntity }, dispatch); } export default connect(mapStateToProps, mapDispatchToProps)(TextEditor);