diff --git a/README.md b/README.md index c222ab6..2debfa1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # mui-rte The Material-UI Rich Text Editor and Viewer - + **mui-rte** is a complete text editor and viewer for `material-ui` v3 and v4 based on `draft-js` and written in Typescript. It is ready to use out of the box yet supports user defined blocks, styles, callbacks, and decorators as well as toolbar and theme customization to enhance the editor to all needs. @@ -50,7 +50,7 @@ Check the [examples](https://github.com/niuware/mui-rte/tree/master/examples) di ## Custom Controls -You can define your custom inline styles, blocks, atomic blocks and callback actions to the editor. Just select an icon from `@material-ui/icons` and define your rules. +You can define your custom inline styles, blocks, atomic blocks and callback actions to the editor. Just select an icon from `@material-ui/icons` or create your own `FunctionComponent` and define your rules. ### Adding a custom inline style @@ -78,7 +78,7 @@ import InvertColorsIcon from '@material-ui/icons/InvertColors' ### Adding a custom block -This sample adds a block to the editor based on a `React Element` defined: +This sample adds a block to the editor based on a `React Element`: ```js import MUIRichTextEditor from 'mui-rte' @@ -111,7 +111,7 @@ const MyBlock = (props) => { ### Adding a custom atomic block -Check [this sample](https://github.com/niuware/mui-rte/blob/master/examples/atomic-custom-block/index.tsx) that shows how to create a control to add a `@material-ui` Card component to the editor. +Check [this sample](https://github.com/niuware/mui-rte/blob/master/examples/atomic-custom-block/index.tsx) that shows how to create a control to add a `@material-ui/core` Card component to the editor. ### Adding a custom callback control @@ -128,7 +128,7 @@ import DoneIcon from '@material-ui/icons/Done' name: "my-callback", icon: , type: "callback", - onClick: (editorState, name) => { + onClick: (editorState, name, anchor) => { console.log(`Clicked ${name} control`) } } @@ -246,6 +246,7 @@ Object.assign(defaultTheme, { |id|`string`|optional|The HTML id attribute for the control| |name|`string`|required|The name of the custom control. For rendering the control this name should be added to the `MUIRichTextEditor` `controls` property.| |icon|`JSX.Element`|optional|The `@material-ui/icons` icon for the control. For "atomic" control type, the icon is not required. [Check this](https://material.io/resources/icons/?style=baseline) for available icons.| +|component|`React.FunctionComponent`|optional|The custom function component for the control. The icon has priority over the component, so if the icon is set the component will be ignored. For "atomic" control type, the component is not required.| |type|`string`|required|Either "inline", "block", "atomic" or "callback"| |inlineStyle|`string`|optional|The `React.CSSProperties` object for styling the text when using a custom inline style.| |blockWrapper|`React.ReactElement`|optional|The custom React component used for rendering a custom block.| @@ -254,6 +255,17 @@ Object.assign(defaultTheme, {
+`TToolbarComponentProps` + +|Property|Type|description| +|---|---|---|---| +|id|string|The id for the component.| +|onMouseDown|(e: React.MouseEvent) => void|The `mousedown` handler.| +|active|boolean|Defines if the block or inline type is active for the current editor selection.| +|disabled|boolean|Sets if the toolbar is disabled.| + +
+ `TDecorator` |Property|Type||description| diff --git a/examples/custom-controls/index.tsx b/examples/custom-controls/index.tsx index ecec318..2fdafdb 100644 --- a/examples/custom-controls/index.tsx +++ b/examples/custom-controls/index.tsx @@ -1,8 +1,8 @@ -import React from 'react' -import MUIRichTextEditor from '../../' +import React, { FunctionComponent } from 'react' +import { Chip, Avatar, Button } from '@material-ui/core' import InvertColorsIcon from '@material-ui/icons/InvertColors' -import TableChartIcon from '@material-ui/icons/TableChart' -import DoneIcon from '@material-ui/icons/Done' +import MUIRichTextEditor from '../../' +import { TToolbarComponentProps } from '../../src/components/Toolbar' const save = (data: string) => { console.log(data) @@ -20,6 +20,32 @@ const MyBlock = (props: any) => { ) } +const MyCallbackComponent: FunctionComponent = (props) => { + return ( + C} + onClick={props.onMouseDown} + label="Callback" + disabled={props.disabled} + /> + ) +} + +const MyBlockComponent: FunctionComponent = (props) => { + return ( + + ) +} + const CustomControls = () => { return ( { }, { name: "my-block", - icon: , + component: MyBlockComponent, type: "block", blockWrapper: }, { name: "my-callback", - icon: , + component: MyCallbackComponent, type: "callback", onClick: (_, name) => { console.log(`Clicked ${name} control`) diff --git a/package.json b/package.json index 3311e85..6c60d0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mui-rte", - "version": "1.8.2", + "version": "1.9.0", "description": "Material-UI Rich Text Editor and Viewer", "keywords": [ "material-ui", diff --git a/src/MUIRichTextEditor.tsx b/src/MUIRichTextEditor.tsx index 2c563ac..24d055d 100644 --- a/src/MUIRichTextEditor.tsx +++ b/src/MUIRichTextEditor.tsx @@ -10,7 +10,7 @@ import { DraftHandleValue, DraftStyleMap, ContentBlock, DraftDecorator, getVisibleSelectionRect, SelectionState } from 'draft-js' -import EditorControls, { TEditorControl, TCustomControl } from './components/EditorControls' +import Toolbar, { TToolbarControl, TCustomControl } from './components/Toolbar' import Link from './components/Link' import Media from './components/Media' import Blockquote from './components/Blockquote' @@ -83,14 +83,14 @@ interface IMUIRichTextEditorProps extends WithStyles { readOnly?: boolean inheritFontSize?: boolean error?: boolean - controls?: Array + controls?: Array onSave?: (data: string) => void onChange?: (state: EditorState) => void customControls?: TCustomControl[], decorators?: TDecorator[] toolbar?: boolean inlineToolbar?: boolean - inlineToolbarControls?: Array + inlineToolbarControls?: Array } type IMUIRichTextEditorState = { @@ -346,7 +346,7 @@ const MUIRichTextEditor: RefForwardingComponent = setEditorState(EditorState.redo(editorState)) } - const handlePrompt = (lastState: EditorState, type: "link" | "media", toolbarMode?: boolean) => { + const handlePrompt = (lastState: EditorState, type: "link" | "media", inlineMode?: boolean) => { const selectionInfo = getSelectionInfo(lastState) const contentState = lastState.getCurrentContent() const linkKey = selectionInfo.linkKey @@ -358,24 +358,24 @@ const MUIRichTextEditor: RefForwardingComponent = setState({ urlData: data, urlKey: linkKey, - toolbarPosition: !toolbarMode ? undefined : state.toolbarPosition, - anchorUrlPopover: !toolbarMode ? document.getElementById(`mui-rte-${type}-control`)! + toolbarPosition: !inlineMode ? undefined : state.toolbarPosition, + anchorUrlPopover: !inlineMode ? document.getElementById(`mui-rte-${type}-control`)! : document.getElementById(`mui-rte-${type}-control-toolbar`)!, urlIsMedia: type === "media" ? true : undefined }) } - const handlePromptForLink = (toolbarMode?: boolean) => { + const handlePromptForLink = (inlineMode?: boolean) => { const selection = editorState.getSelection() if (!selection.isCollapsed()) { - handlePrompt(editorState, "link", toolbarMode) + handlePrompt(editorState, "link", inlineMode) } } - const handlePromptForMedia = (toolbarMode?: boolean, newState?: EditorState) => { + const handlePromptForMedia = (inlineMode?: boolean, newState?: EditorState) => { const lastState = newState || editorState - handlePrompt(lastState, "media", toolbarMode) + handlePrompt(lastState, "media", inlineMode) } const handleConfirmPrompt = (isMedia?: boolean, ...args: any) => { @@ -386,7 +386,7 @@ const MUIRichTextEditor: RefForwardingComponent = confirmLink(...args) } - const handleToolbarClick = (style: string, type: string, id: string, toolbarMode?: boolean) => { + const handleToolbarClick = (style: string, type: string, id: string, inlineMode?: boolean) => { if (type === "inline") { return toggleInlineStyle(style) } @@ -401,10 +401,10 @@ const MUIRichTextEditor: RefForwardingComponent = handleRedo() break case "LINK": - handlePromptForLink(toolbarMode) + handlePromptForLink(inlineMode) break case "IMAGE": - handlePromptForMedia(toolbarMode) + handlePromptForMedia(inlineMode) break case "clear": handleClearFormat() @@ -656,17 +656,17 @@ const MUIRichTextEditor: RefForwardingComponent = top: state.toolbarPosition.top, left: state.toolbarPosition.left }}> - : null} {editable || renderToolbar ? - = (props: IEditorButtonProps) => { - const size = !props.toolbarMode ? "medium" : "small" - const toolbarId = props.toolbarMode ? "-toolbar" : "" - const elemId = props.id + toolbarId - return ( - { - e.preventDefault() - if (props.onClick) { - props.onClick(props.style, props.type, elemId, props.toolbarMode) - } - }} - aria-label={props.label} - color={props.active ? "primary" : "default"} - size={size} - disabled={props.disabled || false} - > - {props.icon} - - ) -} - -export default EditorButton \ No newline at end of file diff --git a/src/components/EditorControls.tsx b/src/components/Toolbar.tsx similarity index 87% rename from src/components/EditorControls.tsx rename to src/components/Toolbar.tsx index 5d0dda1..4f7f46d 100644 --- a/src/components/EditorControls.tsx +++ b/src/components/Toolbar.tsx @@ -16,21 +16,29 @@ import FormatClearIcon from '@material-ui/icons/FormatClear' import SaveIcon from '@material-ui/icons/Save' import UndoIcon from '@material-ui/icons/Undo' import RedoIcon from '@material-ui/icons/Redo' -import EditorButton from './EditorButton' +import ToolbarButton from './ToolbarButton' import { getSelectionInfo } from '../utils' -export type TEditorControl = +export type TToolbarControl = "title" | "bold" | "italic" | "underline" | "link" | "numberList" | "bulletList" | "quote" | "code" | "clear" | "save" | "media" | "strikethrough" | "highlight" | string export type TControlType = "inline" | "block" | "callback" | "atomic" +export type TToolbarComponentProps = { + id: string, + onMouseDown: (e: React.MouseEvent) => void, + active: boolean, + disabled: boolean +} + export type TCustomControl = { id?: string name: string icon?: JSX.Element type: TControlType + component?: FunctionComponent inlineStyle?: React.CSSProperties blockWrapper?: React.ReactElement atomicComponent?: FunctionComponent @@ -39,10 +47,11 @@ export type TCustomControl = { type TStyleType = { id?: string - name: TEditorControl | string + name: TToolbarControl | string label: string style: string - icon: JSX.Element + icon?: JSX.Element + component?: FunctionComponent type: TControlType active?: boolean clickFnName?: string @@ -167,15 +176,15 @@ const STYLE_TYPES: TStyleType[] = [ interface IBlockStyleControlsProps { editorState: EditorState - controls?: Array + controls?: Array customControls?: TCustomControl[] - onClick: (style: string, type: string, id: string, toolbarMode?: boolean) => void - toolbarMode?: boolean + onClick: (style: string, type: string, id: string, inlineMode?: boolean) => void + inlineMode?: boolean className?: string disabled?: boolean } -const EditorControls: FunctionComponent = (props) => { +const Toolbar: FunctionComponent = (props) => { const [availableControls, setAvailableControls] = useState(props.controls ? [] : STYLE_TYPES) const {editorState} = props @@ -192,13 +201,15 @@ const EditorControls: FunctionComponent = (props) => { } else if (props.customControls) { const customControl = props.customControls.find(style => style.name === name) - if (customControl && customControl.type !== "atomic" && customControl.icon) { + if (customControl && customControl.type !== "atomic" && + (customControl.icon || customControl.component)) { filteredControls.push({ - id: customControl.id, + id: customControl.id || (customControl.name + "Id"), name: customControl.name, label: customControl.name, style: customControl.name.toUpperCase(), icon: customControl.icon, + component: customControl.component, type: customControl.type, clickFnName: "onCustomClick" }) @@ -211,7 +222,7 @@ const EditorControls: FunctionComponent = (props) => { return (
{availableControls.map(style => { - if (props.toolbarMode && + if (props.inlineMode && (style.type !== "inline" && (style.name !== "link" && style.name !== "clear"))) { return null } @@ -234,8 +245,8 @@ const EditorControls: FunctionComponent = (props) => { } return ( - = (props) => { style={style.style} type={style.type} icon={style.icon} - toolbarMode={props.toolbarMode} + component={style.component} + inlineMode={props.inlineMode} disabled={props.disabled} /> ) @@ -251,4 +263,4 @@ const EditorControls: FunctionComponent = (props) => {
) } -export default EditorControls \ No newline at end of file +export default Toolbar \ No newline at end of file diff --git a/src/components/ToolbarButton.tsx b/src/components/ToolbarButton.tsx new file mode 100644 index 0000000..d8e2229 --- /dev/null +++ b/src/components/ToolbarButton.tsx @@ -0,0 +1,55 @@ +import React, { FunctionComponent } from 'react' +import { IconButton } from '@material-ui/core' +import { TToolbarComponentProps } from './Toolbar' + +interface IToolbarButtonProps { + id?: string + label: string + style: string + type: string + active?: boolean + icon?: JSX.Element + onClick?: any + inlineMode?: boolean + disabled?: boolean + component?: FunctionComponent +} + +const ToolbarButton: FunctionComponent = (props: IToolbarButtonProps) => { + const size = !props.inlineMode ? "medium" : "small" + const toolbarId = props.inlineMode ? "-toolbar" : "" + const elemId = props.id + toolbarId + const sharedProps = { + id: elemId, + onMouseDown: (e: React.MouseEvent) => { + e.preventDefault() + if (props.onClick) { + props.onClick(props.style, props.type, elemId, props.inlineMode) + } + }, + disabled: props.disabled || false + } + if (props.icon) { + return ( + + {props.icon} + + ) + } + if (props.component) { + return ( + + ) + } + return null +} + +export default ToolbarButton \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index ac6643a..7209ecd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import { EditorState, DraftBlockType, ContentBlock, ContentState, Modifier, SelectionState } from 'draft-js' import Immutable from 'immutable' -import { TCustomControl } from './components/EditorControls' +import { TCustomControl } from './components/Toolbar' export type TSelectionInfo = { inlineStyle: Immutable.OrderedSet,