import * as React from "react"; import * as ReactDOM from "react-dom"; import { Map } from "immutable"; import { ContentBlock, DefaultDraftBlockRenderMap, Editor, EditorState, Modifier, RichUtils, SelectionState, getDefaultKeyBinding, ContentState, RawDraftInlineStyleRange, RawDraftEntityRange, RawDraftEntity, RawDraftContentBlock, RawDraftContentState, DraftBlockType, DraftInlineStyleType, DraftEntityMutability, DraftEntityType, convertFromHTML, convertToRaw } from 'draft-js'; const SPLIT_HEADER_BLOCK = 'split-header-block'; export type KeyName = 'ENTER'; export type KeyCode = number; export const KEYCODES: Record = { ENTER: 13, }; type SyntheticKeyboardEvent = React.KeyboardEvent<{}>; class RichEditorExample extends React.Component<{}, { editorState: EditorState }> { constructor() { super({}); const sampleMarkup = 'Bold text, Italic text

' + 'Example link

' + ''; const blocksFromHTML = convertFromHTML(sampleMarkup); const state = ContentState.createFromBlockArray( blocksFromHTML.contentBlocks, blocksFromHTML.entityMap, ); this.state = { editorState: EditorState.createWithContent(state) }; } onChange: (editorState: EditorState) => void = (editorState: EditorState) => this.setState({ editorState }); keyBindingFn(e: SyntheticKeyboardEvent): string { if (e.keyCode === KEYCODES.ENTER) { const { editorState } = this.state; const contentState = editorState.getCurrentContent(); const selectionState = editorState.getSelection(); // only split headers into header and unstyled if we press 'Enter' // at the end of a header (without text selected) if (selectionState.isCollapsed()) { const endKey = selectionState.getEndKey(); const endOffset = selectionState.getEndOffset(); const endBlock = contentState.getBlockForKey(endKey); if (isHeaderBlock(endBlock) && endOffset === endBlock.getText().length) { return SPLIT_HEADER_BLOCK; } } } return getDefaultKeyBinding(e); } handleKeyCommand = (command: string, editorState: EditorState) => { if (command === SPLIT_HEADER_BLOCK) { this.onChange(this.splitHeaderToNewBlock()); return 'handled'; } const newState = RichUtils.handleKeyCommand(editorState, command); if (newState) { this.onChange(newState); return "handled"; } return "not-handled"; } toggleBlockType: (blockType: string) => void = (blockType: string) => { this.onChange(RichUtils.toggleBlockType(this.state.editorState, blockType)); } toggleInlineStyle: (inlineStyle: string) => void = (inlineStyle: string) => { this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, inlineStyle)); } splitHeaderToNewBlock(): EditorState { const { editorState } = this.state; const selection = editorState.getSelection(); // Add a new block after the cursor const contentWithBlock = Modifier.splitBlock( editorState.getCurrentContent(), selection, ); // Change the new block type to be normal 'unstyled' text, const newBlock = contentWithBlock.getBlockAfter(selection.getEndKey()); const contentWithUnstyledBlock = Modifier.setBlockType( contentWithBlock, SelectionState.createEmpty(newBlock.getKey()), 'unstyled', ); // push the new state with 'insert-characters' to preserve the undo/redo stack const stateWithNewline = EditorState.push( editorState, contentWithUnstyledBlock, 'insert-characters' ); // manually move the cursor to the next line (as expected) const nextState = EditorState.forceSelection( stateWithNewline, SelectionState.createEmpty(newBlock.getKey()), ); return nextState; } render(): React.ReactElement<{}> { // If the user changes block type before entering any text, we can // either style the placeholder or hide it. Let's just hide it now. let className = 'RichEditor-editor'; var contentState = this.state.editorState.getCurrentContent(); if (!contentState.hasText()) { if (contentState.getBlockMap().first().getType() !== 'unstyled') { className += ' RichEditor-hidePlaceholder'; } } return (
); } } // Custom overrides for "code" style. const styleMap = { CODE: { backgroundColor: 'rgba(0, 0, 0, 0.05)', fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace', fontSize: 16, padding: 2, }, }; function getBlockStyle(block: ContentBlock) { switch (block.getType()) { case 'blockquote': return 'RichEditor-blockquote'; default: return null; } } interface Props { key: string active: boolean label: string onToggle: (blockType: string) => void style: string } class StyleButton extends React.Component { constructor(props: Props) { super(props); } onToggle: (event: Event) => void = (event: Event) => { event.preventDefault(); this.props.onToggle(this.props.style); }; render(): React.ReactElement<{}> { let className = 'RichEditor-styleButton'; if (this.props.active) { className += ' RichEditor-activeButton'; } return ( this.onToggle(e as any)}> {this.props.label} ); } } const BLOCK_TYPES = [ { label: 'H1', style: 'header-one' }, { label: 'H2', style: 'header-two' }, { label: 'H3', style: 'header-three' }, { label: 'H4', style: 'header-four' }, { label: 'H5', style: 'header-five' }, { label: 'H6', style: 'header-six' }, { label: 'Blockquote', style: 'blockquote' }, { label: 'UL', style: 'unordered-list-item' }, { label: 'OL', style: 'ordered-list-item' }, { label: 'Code Block', style: 'code-block' }, ]; const isHeaderBlock = (block: ContentBlock): boolean => { switch (block.getType()) { case 'header-one': case 'header-two': case 'header-three': case 'header-four': case 'header-five': case 'header-six': { return true; } default: return false; } } const BlockStyleControls = (props: {editorState: EditorState, onToggle: (blockType: string) => void}) => { const {editorState} = props; const selection = editorState.getSelection(); const blockType = editorState .getCurrentContent() .getBlockForKey(selection.getStartKey()) .getType(); return (
{BLOCK_TYPES.map((type) => ) }
); }; var INLINE_STYLES = [ { label: 'Bold', style: 'BOLD' }, { label: 'Italic', style: 'ITALIC' }, { label: 'Underline', style: 'UNDERLINE' }, { label: 'Monospace', style: 'CODE' }, ]; const InlineStyleControls = (props: {editorState: EditorState, onToggle: (blockType: string) => void}) => { var currentStyle = props.editorState.getCurrentInlineStyle(); return (
{INLINE_STYLES.map(type => ) }
); }; ReactDOM.render( , document.getElementById('target') ); const editorState = EditorState.createEmpty(); const contentState = editorState.getCurrentContent(); const rawContentState: RawDraftContentState = convertToRaw(contentState); rawContentState.blocks.forEach((block: RawDraftContentBlock) => { block.entityRanges.forEach((entityRange: RawDraftEntityRange) => { const { key, offset, length } = entityRange; const entity: RawDraftEntity = rawContentState.entityMap[key]; const entityType: DraftEntityType = entity.type; const entityMutability: DraftEntityMutability = entity.mutability; console.log(entityType, entityMutability, offset, length); }); block.inlineStyleRanges.forEach((inlineStyleRange: RawDraftInlineStyleRange) => { const { offset, length } = inlineStyleRange const inlineStyle: DraftInlineStyleType = inlineStyleRange.style; console.log(inlineStyle, offset, length); }); });