Skip to content

Commit

Permalink
Add basic 'Image' control
Browse files Browse the repository at this point in the history
- Enable custom control ordering
  • Loading branch information
niuware committed Jun 28, 2019
1 parent 504528b commit e4e8b99
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 47 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mui-rte",
"version": "1.0.12",
"version": "1.0.13",
"description": "Material-UI Rich Text Editor and Viewer",
"keywords": [
"material-ui",
Expand Down
3 changes: 2 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ export default {
include: ['node_modules/**'],
extensions: ['.js', '.ts']
}),
uglify()
process.env.NODE_ENV === "production" && uglify()
],
output: {
file: 'dist/index.js',
format: 'cjs',
exports: 'named',
sourcemap: process.env.NODE_ENV === "development"
}
};
150 changes: 120 additions & 30 deletions src/MUIRichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,28 @@ import * as React from 'react'
import Immutable from 'immutable'
import classNames from 'classnames'
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles'
import SaveIcon from '@material-ui/icons/Save'
import FormatClearIcon from '@material-ui/icons/FormatClear'
import {
Editor, EditorState, convertFromRaw, RichUtils,
CompositeDecorator, convertToRaw, DefaultDraftBlockRenderMap
Editor, EditorState, convertFromRaw, RichUtils, AtomicBlockUtils,
CompositeDecorator, convertToRaw, DefaultDraftBlockRenderMap, DraftEditorCommand,
DraftHandleValue,
ContentBlock
} from 'draft-js'
import EditorControls, { TEditorControl } from './components/EditorControls'
import EditorButton from './components/EditorButton'
import Link from './components/Link'
import LinkPopover from './components/LinkPopover'
import Image from './components/Image'
import Blockquote from './components/Blockquote'
import CodeBlock from './components/CodeBlock'
import UrlPopover from './components/UrlPopover'
import { getSelectionInfo, getCompatibleSpacing } from './utils'

const styles = ({ spacing, typography, palette }: Theme) => createStyles({
root: {
margin: getCompatibleSpacing(spacing, 1, 0, 0, 0),
fontFamily: typography.body1.fontFamily,
fontSize: typography.body1.fontSize
fontSize: typography.body1.fontSize,
'& figure': {
margin: 0
}
},
inheritFontSize: {
fontSize: "inherit"
Expand Down Expand Up @@ -70,6 +73,7 @@ type IMUIRichTextEditorState = {
editorState: EditorState
focused: boolean
anchorLinkPopover?: HTMLElement
anchorMediaPopover?: HTMLElement
urlValue?: string
urlKey?: string
}
Expand All @@ -92,7 +96,7 @@ class MUIRichTextEditor extends React.Component<IMUIRichTextEditorProps, IMUIRic
{
strategy: this.findLinkEntities,
component: Link,
},
}
])
let editorState = EditorState.createEmpty(decorator)
if (this.props.value) {
Expand Down Expand Up @@ -143,6 +147,15 @@ class MUIRichTextEditor extends React.Component<IMUIRichTextEditorProps, IMUIRic
})
}

handleKeyCommand = (command: DraftEditorCommand, editorState: EditorState): DraftHandleValue => {
const newState = RichUtils.handleKeyCommand(editorState, command)
if (newState) {
this.handleChange(newState)
return "handled"
}
return "not-handled"
}

save = () => {
if (this.props.onSave) {
this.props.onSave(JSON.stringify(convertToRaw(this.state.editorState.getCurrentContent())))
Expand Down Expand Up @@ -204,7 +217,7 @@ class MUIRichTextEditor extends React.Component<IMUIRichTextEditorProps, IMUIRic
removeLink = () => {
const { editorState } = this.state
const selection = editorState.getSelection()
this.updateStateForLink(RichUtils.toggleLink(editorState, selection, null))
this.updateStateForPopover(RichUtils.toggleLink(editorState, selection, null))
}

confirmLink = (url?: string) => {
Expand Down Expand Up @@ -245,13 +258,74 @@ class MUIRichTextEditor extends React.Component<IMUIRichTextEditorProps, IMUIRic
newEditorState.getSelection(),
entityKey)
}
this.updateStateForLink(replaceEditorState)
this.updateStateForPopover(replaceEditorState)
}

promptForMedia = () => {
const { editorState } = this.state
let url = ''
let urlKey = undefined
const selectionInfo = getSelectionInfo(editorState)
const contentState = editorState.getCurrentContent()
const linkKey = selectionInfo.linkKey

if (linkKey) {
const linkInstance = contentState.getEntity(linkKey)
url = linkInstance.getData().url
urlKey = linkKey
}

this.setState({
urlValue: url,
urlKey: urlKey,
anchorMediaPopover: document.getElementById("mui-rte-image-control")!
}, () => {
setTimeout(() => document.getElementById("mui-rte-media-popover")!.focus(), 0)
})
}

confirmMedia = (url?: string) => {
const { editorState, urlKey } = this.state
if (!url) {
this.setState({
anchorMediaPopover: undefined
})
return
}

const contentState = editorState.getCurrentContent()
let replaceEditorState = null

if (urlKey) {
contentState.replaceEntityData(urlKey, {
url: url
})
const newEditorState = EditorState.push(editorState, contentState, "apply-entity")
replaceEditorState = EditorState.forceSelection(newEditorState, newEditorState.getCurrentContent().getSelectionAfter())
}
else {
const contentStateWithEntity = contentState.createEntity(
'IMAGE',
'IMMUTABLE',
{
url: url
}
)
const entityKey = contentStateWithEntity.getLastCreatedEntityKey()
const newEditorStateRaw = EditorState.set(editorState, { currentContent: contentStateWithEntity})
const newEditorState = AtomicBlockUtils.insertAtomicBlock(
newEditorStateRaw,
entityKey, ' ')
replaceEditorState = EditorState.forceSelection(newEditorState, newEditorState.getCurrentContent().getSelectionAfter())
}
this.updateStateForPopover(replaceEditorState)
}

updateStateForLink = (editorState: EditorState) => {
updateStateForPopover = (editorState: EditorState) => {
this.setState({
editorState: editorState,
anchorLinkPopover: undefined,
anchorMediaPopover: undefined,
urlValue: undefined,
urlKey: undefined
}, () => {
Expand All @@ -272,6 +346,25 @@ class MUIRichTextEditor extends React.Component<IMUIRichTextEditorProps, IMUIRic
)
}

blockRenderer = (contentBlock: ContentBlock) => {
const blockType = contentBlock.getType()
if (blockType === 'atomic') {
const contentState = this.state.editorState.getCurrentContent()
const entity = contentBlock.getEntityAt(0)
if (!entity) {
return null
}
const type = contentState.getEntity(entity).getType()
if (type === 'IMAGE') {
return {
component: Image,
editable: false
}
}
return null
}
}

render() {
const { classes, controls } = this.props
const contentState = this.state.editorState.getCurrentContent()
Expand Down Expand Up @@ -302,25 +395,11 @@ class MUIRichTextEditor extends React.Component<IMUIRichTextEditorProps, IMUIRic
onToggleBlock={this.toggleBlockType}
onToggleInline={this.toggleInlineStyle}
onPromptLink={this.promptForLink}
onPromptMedia={this.promptForMedia}
onClear={this.handleClearFormat}
onSave={this.save}
controls={controls}
>
{this.props.controls === undefined || this.props.controls.includes("clear") ?
<EditorButton
key="clear"
label="Format Clear"
onClick={this.handleClearFormat}
icon={<FormatClearIcon />}
/>
: null }
{this.props.controls === undefined || this.props.controls.includes("save") ?
<EditorButton
key="save"
label="Save"
onClick={this.save}
icon={<SaveIcon />}
/>
: null }
</EditorControls>
/>
: null}
{placeholder}
<div className={classNames(className, classes.editor, {
Expand All @@ -329,19 +408,30 @@ class MUIRichTextEditor extends React.Component<IMUIRichTextEditorProps, IMUIRic
})} onClick={this.handleFocus} onBlur={this.handleBlur}>
<Editor
blockRenderMap={this.extendedBlockRenderMap}
blockRendererFn={this.blockRenderer}
editorState={this.state.editorState}
onChange={this.handleChange}
readOnly={this.props.readOnly}
handleKeyCommand={this.handleKeyCommand}
ref="editor"
/>
</div>
{this.state.anchorLinkPopover ?
<LinkPopover
<UrlPopover
id="mui-rte-link-popover"
url={this.state.urlValue}
anchor={this.state.anchorLinkPopover}
onConfirm={this.confirmLink}
/>
: null}
{this.state.anchorMediaPopover ?
<UrlPopover
id="mui-rte-media-popover"
url={this.state.urlValue}
anchor={this.state.anchorMediaPopover}
onConfirm={this.confirmMedia}
/>
: null}
</div>
)
}
Expand Down
51 changes: 44 additions & 7 deletions src/components/EditorControls.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
import * as React from 'react'
import { EditorState } from 'draft-js'
import FormatBoldIcon from '@material-ui/icons/FormatBold'
import FormatItalicIcon from '@material-ui/icons/FormatItalic'
import FormatUnderlinedIcon from '@material-ui/icons/FormatUnderlined'
import TitleIcon from '@material-ui/icons/Title'
import InsertLinkIcon from '@material-ui/icons/InsertLink'
import InsertPhotoIcon from '@material-ui/icons/InsertPhoto'
import FormatListNumberedIcon from '@material-ui/icons/FormatListNumbered'
import FormatListBulletedIcon from '@material-ui/icons/FormatListBulleted'
import FormatQuoteIcon from '@material-ui/icons/FormatQuote'
import CodeIcon from '@material-ui/icons/Code'
import { EditorState } from 'draft-js'
import FormatClearIcon from '@material-ui/icons/FormatClear'
import SaveIcon from '@material-ui/icons/Save'
import EditorButton from './EditorButton'
import { getSelectionInfo } from '../utils'

type KeyString = {
[key: string]: React.ReactNode | EditorState
}

export type TEditorControl = "title" | "bold" | "italic" | "underline" | "link" | "numberList" | "bulletList" | "quote" | "code" | "clear" | "save"
export type TEditorControl =
"title" | "bold" | "italic" | "underline" | "link" | "numberList" |
"bulletList" | "quote" | "code" | "clear" | "save" | "image"

type TStyleType = {
id?: string
name: TEditorControl
label: string
style: string
icon: JSX.Element
type: "inline" | "block" | "decorator"
type: "inline" | "block" | "callback"
active?: boolean
clickFnName?: string
}
Expand Down Expand Up @@ -63,10 +68,19 @@ const STYLE_TYPES: TStyleType[] = [
name: "link",
style: 'LINK',
icon: <InsertLinkIcon />,
type: "decorator",
type: "callback",
clickFnName: "onPromptLink",
id: "mui-rte-link-control"
},
{
label: 'Image',
name: "image",
style: 'IMAGE',
icon: <InsertPhotoIcon />,
clickFnName: "onPromptMedia",
type: "callback",
id: "mui-rte-image-control"
},
{
label: 'OL',
name: "bulletList",
Expand Down Expand Up @@ -94,6 +108,22 @@ const STYLE_TYPES: TStyleType[] = [
style: 'code-block',
icon: <CodeIcon />,
type: "block"
},
{
label: 'Clear',
name: "clear",
style: 'clear',
icon: <FormatClearIcon />,
type: "callback",
clickFnName: "onClear"
},
{
label: 'Save',
name: "save",
style: 'save',
icon: <SaveIcon />,
type: "callback",
clickFnName: "onSave"
}
]

Expand All @@ -104,14 +134,22 @@ interface IBlockStyleControlsProps extends KeyString {
onToggleInline: (inlineStyle: any) => void
onToggleBlock: (blockType: any) => void
onPromptLink: () => void
onPromptMedia: () => void
onClear: () => void
onSave: () => void
}

const EditorControls: React.FC<IBlockStyleControlsProps> = (props: IBlockStyleControlsProps) => {
const selectionInfo = getSelectionInfo(props.editorState)
let filteredControls = STYLE_TYPES
if (props.controls) {
filteredControls = STYLE_TYPES.filter(style => {
return props.controls!.includes(style.name)
filteredControls = []

props.controls!.forEach(name => {
const style = STYLE_TYPES.find(style => style.name === name)
if (style) {
filteredControls.push(style)
}
})
}
return (
Expand Down Expand Up @@ -144,7 +182,6 @@ const EditorControls: React.FC<IBlockStyleControlsProps> = (props: IBlockStyleCo
/>
)
})}
{props.children}
</div>
)
}
Expand Down
Loading

0 comments on commit e4e8b99

Please sign in to comment.