Skip to content

Instantly share code, notes, and snippets.

@aberba
Forked from paulinep/RichEditor.js
Created December 30, 2020 22:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aberba/caec4cfe2e451b0f84738776b426f4c4 to your computer and use it in GitHub Desktop.
Save aberba/caec4cfe2e451b0f84738776b426f4c4 to your computer and use it in GitHub Desktop.
Draft js editor with images and videos
import React, {Component} from 'react'
import ReactDOM from 'react-dom';
import { EditorState, RichUtils,Editor, ContentState, DefaultDraftBlockRenderMap, convertFromHTML, getSafeBodyFromHTML, AtomicBlockUtils, CompositeDecorator} from 'draft-js';
import createImagePlugin from "draft-js-image-plugin";
import './rich-editor.scss'
import * as Immutable from "immutable";
import {stateToHTML} from 'draft-js-export-html';
import {InlineStyleControls, BlockStyleControls} from './StyleButton'
import Svg from 'common/Svg';
import createVideoPlugin from 'draft-js-video-plugin';
const videoPlugin = createVideoPlugin();
const imagePlugin = createImagePlugin();
const plugins = [imagePlugin, videoPlugin];
export default class DraftEditor extends React.Component {
constructor(props) {
super(props);
const decorator = new CompositeDecorator([
{
strategy: findLinkEntities,
component: Link,
},
{
strategy: findImageEntities,
component: Image,
},
{
strategy: findVideoEntities,
component: Video,
},
]);
let state;
//Если HTML пришел
if(/^\s*</.test(props.sampleMarkup)){
const blocksFromHTML = convertFromHTML(props.sampleMarkup || '')
console.log(props.sampleMarkup, blocksFromHTML)
state = ContentState.createFromBlockArray(
blocksFromHTML.contentBlocks,
blocksFromHTML.entityMap
);
}else{
state= ContentState.createFromText(props.sampleMarkup || '')
}
this.state = {
editorState: EditorState.createWithContent(
state,
decorator,
),
showURLInput: false,
showMediaInput: false,
urlValue: '',
};
this.focus = () => this.refs.editor.focus();
this.onChange = this._onChange.bind(this)
this.promptForLink = this._promptForLink.bind(this);
this.onURLChange = (e) => this.setState({urlValue: e.target.value});
this.confirmLink = this._confirmLink.bind(this);
this.onLinkInputKeyDown = this._onLinkInputKeyDown.bind(this);
this.removeLink = this._removeLink.bind(this);
this.toggleBlockType = this._toggleBlockType.bind(this);
this.toggleInlineStyle = this._toggleInlineStyle.bind(this);
this.promptForMedia = this._promptForMedia.bind(this);
this.confirmMedia = this._confirmMedia.bind(this);
}
_onChange = (editorState) => {
this.setState({ editorState: editorState });
let options = {
blockRenderers: {
atomic: (block) => {
let entity;
let key= block.getEntityAt(0);
if(key){
entity = editorState.getCurrentContent().getEntity(key);
}else{
const contentStateWithEntity = editorState.getCurrentContent().createEntity("VIDEO", "IMMUTABLE", {
});
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
entity = editorState.getCurrentContent().getEntity(entityKey);
}
if (entity.getType() === 'VIDEO') {
const {src} = entity.getData();
return '<figure><iframe width="560" height="315" src="'+src+'" frameBorder="0" allowfullscreen></iframe></figure>';
}
},
},
};
this.props.onChangeText('text',stateToHTML(editorState.getCurrentContent(), options))
}
_promptForLink(e) {
e.preventDefault();
const {editorState} = this.state;
const selection = editorState.getSelection();
if (!selection.isCollapsed()) {
const contentState = editorState.getCurrentContent();
const startKey = editorState.getSelection().getStartKey();
const startOffset = editorState.getSelection().getStartOffset();
const blockWithLinkAtBeginning = contentState.getBlockForKey(startKey);
const linkKey = blockWithLinkAtBeginning.getEntityAt(startOffset);
let url = '';
if (linkKey) {
const linkInstance = contentState.getEntity(linkKey);
url = linkInstance.getData().url;
}
this.setState({
showURLInput: true,
urlValue: url,
}, () => {
setTimeout(() => this.refs.url.focus(), 0);
});
}
}
_confirmLink(e) {
e.preventDefault();
const {editorState, urlValue} = this.state;
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
'LINK',
'MUTABLE',
{url: urlValue}
);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const newEditorState = EditorState.set(editorState, { currentContent: contentStateWithEntity });
this.setState({
editorState: RichUtils.toggleLink(
newEditorState,
newEditorState.getSelection(),
entityKey
),
showURLInput: false,
urlValue: '',
}, () => {
setTimeout(() => this.refs.editor.focus(), 0);
});
}
_onLinkInputKeyDown(e) {
if (e.which === 13) {
this._confirmLink(e);
}
}
_removeLink(e) {
e.preventDefault();
const {editorState} = this.state;
const selection = editorState.getSelection();
if (!selection.isCollapsed()) {
this.setState({
editorState: RichUtils.toggleLink(editorState, selection, null),
}, () => {
setTimeout(() => this.refs.editor.focus(), 0);
});
}
}
_toggleBlockType(blockType) {
this.onChange(
RichUtils.toggleBlockType(
this.state.editorState,
blockType
)
);
}
_toggleInlineStyle(inlineStyle) {
this.onChange(
RichUtils.toggleInlineStyle(
this.state.editorState,
inlineStyle
)
);
}
onAddImage=(e) => {
e.preventDefault();
const editorState = this.state.editorState;
const reader = new FileReader()
const file = e.target.files[0]
if (!file) return
reader.onloadend = () => {
const urlValue = reader.result
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
"IMAGE",
"IMMUTABLE",
{src: urlValue}
);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const newEditorState = EditorState.set(
editorState,
{currentContent: contentStateWithEntity},
"create-entity"
);
this.setState(
{
editorState: AtomicBlockUtils.insertAtomicBlock(
newEditorState,
entityKey,
" "
)
},
() => {
setTimeout(() => this.focus(), 0);
}
);
}
reader.readAsDataURL(file)
};
_confirmMedia(e) {
e.preventDefault();
const {editorState, urlValue} = this.state;
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
'VIDEO',
'IMMUTABLE',
{src: urlValue}
);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const newEditorState = EditorState.set(
editorState,
{currentContent: contentStateWithEntity},
"create-entity"
);
this.setState({
editorState: AtomicBlockUtils.insertAtomicBlock(
newEditorState,
entityKey,
' '
),
showMediaInput: false,
urlValue: '',
}, () => {
setTimeout(() => this.focus(), 0);
});
}
_promptForMedia() {
const {editorState} = this.state;
this.setState({
showMediaInput: true,
urlValue: '',
}, () => {
setTimeout(() => this.refs.url.focus(), 0);
});
}
render() {
const {editorState} = this.state;
let urlInput;
if (this.state.showURLInput || this.state.showMediaInput) {
urlInput =
<div style={styles.urlInputContainer}>
<input
onChange={this.onURLChange}
ref="url"
style={styles.urlInput}
type="text"
value={this.state.urlValue}
onKeyDown={this.onLinkInputKeyDown}
/>
{!this.state.showMediaInput ?
(<React.Fragment><button onMouseDown={this.confirmLink}>
Вставить
</button>
<button onClick={(e)=>this.removeLink(e)}>
Удалить
</button></React.Fragment>):
<button onMouseDown={this.confirmMedia}>
Вставить
</button>}
</div>;
}
return (
<div style={styles.root}>
<div style={{marginBottom: 10}}>
<BlockStyleControls
editorState={editorState}
onToggle={this.toggleBlockType}
/>
<InlineStyleControls
editorState={editorState}
onToggle={this.toggleInlineStyle}
/>
<label>
<Svg name={'image_editor_button'} width={47} height={47}/>
<input onChange={(e)=>this.onAddImage(e)} placeholder={'image'} type="file" className="feed-form__file-input"/>
</label>
<label>
<Svg name={'link_button'} onClick={this.promptForLink} width={47} height={47}/>
</label>
<label>
<Svg name={'editor_video'} onClick={this.promptForMedia} width={47} height={47}/>
</label>
</div>
{urlInput}
<div style={styles.editor} onClick={this.focus}>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={plugins}
blockRendererFn={mediaBlockRenderer}
ref="editor"
/>
</div>
</div>
);
}
}
function findLinkEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
);
},
callback
);
}
const Link = (props) => {
const {url} = props.contentState.getEntity(props.entityKey).getData();
return (
<a href={url} target="_blank" style={styles.link}>
{props.children}
</a>
);
};
function findImageEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'IMAGE'
);
},
callback
);
}
function findVideoEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'VIDEO'
);
},
callback
);
}
const Image = (props) => {
return <img src={props.src} style={styles.media} />;
};
function mediaBlockRenderer(block) {
if (block.getType() === 'atomic') {
return {
component: Media,
editable: false,
};
}
return null;
}
const Video = (props) => {
return <iframe width="560" height="315" src={props.src} frameBorder="0" allowFullScreen></iframe>
};
const Media = (props) => {
let entity;
let key= props.block.getEntityAt(0);
if(key){
entity = props.contentState.getEntity(key);
}else{
const contentStateWithEntity = props.contentState.createEntity("VIDEO", "IMMUTABLE", { src: 'src'
});
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
entity = props.contentState.getEntity(entityKey);
}
const {src} = entity.getData();
const type = entity.getType();
let media;
if (type === 'IMAGE') {
media = <Image src={src} />;
} else if (type === 'VIDEO' ) {
media = <Video src={src} />;
}else{
console.log(type)
return null;
}
return media;
};
const styles = {
root: {
fontFamily: '\'Helvetica\', sans-serif',
padding: 20,
width: 600,
},
editor: {
border: '1px solid #ccc',
cursor: 'text',
minHeight: 80,
padding: 10,
},
button: {
marginTop: 10,
textAlign: 'center',
},
urlInputContainer: {
marginBottom: 10,
},
urlInput: {
fontFamily: '\'Georgia\', serif',
marginRight: 10,
padding: 3,
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment