diff --git a/examples/autocomplete-async/index.tsx b/examples/autocomplete-async/index.tsx new file mode 100644 index 0000000..07ca5d7 --- /dev/null +++ b/examples/autocomplete-async/index.tsx @@ -0,0 +1,124 @@ +import React, { FunctionComponent } from 'react' +import ListItemText from '@mui/material/ListItemText' +import ListItemAvatar from '@mui/material/ListItemAvatar' +import Avatar from '@mui/material/Avatar' +import MUIRichTextEditor, { TAutocompleteItem } from '../../' + +const save = (data: string) => { + console.log(data) +} + +type TStaff = { + avatar: string + name: string +} + +const Staff: FunctionComponent = (props) => { + return ( + <> + + {props.name.substr(0, 1)} + + + + ) +} + +const emojis: TAutocompleteItem[] = [ + { + keys: ["face", "grin"], + value: "😀", + content: "😀", + }, + { + keys: ["face", "beaming"], + value: "😁", + content: "😁", + }, + { + keys: ["face", "joy"], + value: "😂", + content: "😂", + }, + { + keys: ["face", "grin", "big"], + value: "😃", + content: "😃", + }, + { + keys: ["face", "grin", "smile"], + value: "😄", + content: "😄", + }, + { + keys: ["face", "sweat"], + value: "😅", + content: "😅", + } +] + +const cities: TAutocompleteItem[] = [ + { + keys: ["mexico"], + value: "Mexico City", + content: "Mexico City", + }, + { + keys: ["mexico", "beach"], + value: "Cancun", + content: "Cancun", + }, + { + keys: ["japan", "olympics"], + value: "Tokyo", + content: "Tokyo", + }, + { + keys: ["japan"], + value: "Osaka", + content: "Osaka", + } +] + +const searchUsers = async (query: string): Promise => { + let response = await fetch(`https://reqres.in/api/users?page=${query.length - 2}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const json = await response.json(); + return json.data!.map((u:any) => {return { + keys: [u.email, u.first_name, u.last_name], + value: `${u.first_name}`, + content: , + }}); +}; + +const Autocomplete = () => { + return ( + + ) +} + +export default Autocomplete diff --git a/examples/autocomplete/index.tsx b/examples/autocomplete/index.tsx index 5b2f884..ec7c967 100644 --- a/examples/autocomplete/index.tsx +++ b/examples/autocomplete/index.tsx @@ -110,6 +110,7 @@ const Autocomplete = () => { label="Try typing ':grin' or '/mexico'..." onSave={save} autocomplete={{ + minSearchCharCount: 2, strategies: [ { items: emojis, diff --git a/examples/main.tsx b/examples/main.tsx index c402b6c..f7372d5 100644 --- a/examples/main.tsx +++ b/examples/main.tsx @@ -15,6 +15,7 @@ import AtomicCustomBlock from './atomic-custom-block' import KeyBindings from './key-bindings' import MaxLength from './max-length' import Autocomplete from './autocomplete' +import AutocompleteAsync from './autocomplete-async' import AutocompleteAtomic from './autocomplete-atomic' import AsyncImageUpload from './async-image-upload' import AsyncAtomicCustomBlock from './async-atomic-custom-block' @@ -51,6 +52,7 @@ const App = () => { +
Promise triggerChar: string; - items: TAutocompleteItem[]; + items?: TAutocompleteItem[]; insertSpaceAfter?: boolean; atomicBlockName?: string; }; export declare type TAutocomplete = { + minSearchCharCount?: number; strategies: TAutocompleteStrategy[]; suggestLimit?: number; }; @@ -96,6 +98,7 @@ export declare type TAsyncAtomicBlockResponse = { export declare type TMUIRichTextEditorRef = { focus: () => void; save: () => void; + insertText: (text: string) => void; /** * @deprecated Use `insertAtomicBlockSync` instead. */ diff --git a/index.js b/index.js index b9a001d..2671ff1 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -export {default} from './dist/MUIRichTextEditor' +module.exports = require('./dist/MUIRichTextEditor') diff --git a/src/MUIRichTextEditor.tsx b/src/MUIRichTextEditor.tsx index 0aeb4f9..3aa8ffe 100644 --- a/src/MUIRichTextEditor.tsx +++ b/src/MUIRichTextEditor.tsx @@ -28,13 +28,15 @@ export type TDecorator = { } export type TAutocompleteStrategy = { + asyncItems?: (search: string) => Promise triggerChar: string - items: TAutocompleteItem[] + items?: TAutocompleteItem[] insertSpaceAfter?: boolean atomicBlockName?: string } export type TAutocomplete = { + minSearchCharCount?: number; strategies: TAutocompleteStrategy[] suggestLimit?: number } @@ -130,6 +132,41 @@ interface TMUIRichTextEditorStyles { } } +// styling for multilevel (tabbed) lists from draft-js +const listStyles = { + '& .public-DraftStyleDefault-ol,.public-DraftStyleDefault-ul': {margin: '16px 0', padding: 0}, + '& .public-DraftStyleDefault-ltr': {direction: 'ltr', textAlign: 'left'}, + '& .public-DraftStyleDefault-rtl': {direction :'rtl', textAlign: 'right'}, + '& .public-DraftStyleDefault-listLTR': {direction: 'ltr'}, + '& .public-DraftStyleDefault-listRTL': {direction: 'rtl'}, + '& .public-DraftStyleDefault-depth0.public-DraftStyleDefault-listLTR': {marginLeft: '1.5em'}, + '& .public-DraftStyleDefault-depth1.public-DraftStyleDefault-listLTR': {marginLeft: '3em'}, + '& .public-DraftStyleDefault-depth2.public-DraftStyleDefault-listLTR': {marginLeft: '4.5em'}, + '& .public-DraftStyleDefault-depth3.public-DraftStyleDefault-listLTR': {marginLeft: '6em'}, + '& .public-DraftStyleDefault-depth4.public-DraftStyleDefault-listLTR': {marginLeft: '7.5em'}, + '& .public-DraftStyleDefault-depth0.public-DraftStyleDefault-listRTL': {marginRight: '1.5em'}, + '& .public-DraftStyleDefault-depth1.public-DraftStyleDefault-listRTL': {marginRight: '3em'}, + '& .public-DraftStyleDefault-depth2.public-DraftStyleDefault-listRTL': {marginRight: '4.5em'}, + '& .public-DraftStyleDefault-depth3.public-DraftStyleDefault-listRTL': {marginRight: '6em'}, + '& .public-DraftStyleDefault-depth4.public-DraftStyleDefault-listRTL': {marginRight: '7.5em'}, + '& .public-DraftStyleDefault-unorderedListItem': {listStyleType: 'square', position: 'relative'}, + '& .public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth0': {listStyleType: 'disc'}, + '& .public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth1': {listStyleType: 'circle'}, + '& .public-DraftStyleDefault-orderedListItem': {listStyleType: 'none', position: 'relative'}, + '& .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listLTR:before': {left: '-36px', position: 'absolute', textAlign: 'right', width: '30px'}, + '& .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listRTL:before': {position: 'absolute', right: '-36px', textAlign: 'left', width: '30px'}, + '& .public-DraftStyleDefault-orderedListItem:before': {content: 'counter(ol0) ". "', counterIncrement: 'ol0'}, + '& .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth1:before': {content: 'counter(ol1,lower-alpha) ". "', counterIncrement: 'ol1'}, + '& .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth2:before': {content: 'counter(ol2,lower-roman) ". "', counterIncrement: 'ol2'}, + '& .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth3:before': {content: 'counter(ol3) ". "', counterIncrement: 'ol3'}, + '& .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth4:before': {content: 'counter(ol4,lower-alpha) ". "', counterIncrement: 'ol4'}, + '& .public-DraftStyleDefault-depth0.public-DraftStyleDefault-reset': {counterReset: 'ol0'}, + '& .public-DraftStyleDefault-depth1.public-DraftStyleDefault-reset': {counterReset: 'ol1'}, + '& .public-DraftStyleDefault-depth2.public-DraftStyleDefault-reset': {counterReset: 'ol2'}, + '& .public-DraftStyleDefault-depth3.public-DraftStyleDefault-reset': {counterReset: 'ol3'}, + '& .public-DraftStyleDefault-depth4.public-DraftStyleDefault-reset': {counterReset: 'ol4'}, +} + const styles = (theme: Theme & TMUIRichTextEditorStyles) => createStyles({ root: theme?.overrides?.MUIRichTextEditor?.root || {}, container: theme?.overrides?.MUIRichTextEditor?.container || { @@ -144,7 +181,9 @@ const styles = (theme: Theme & TMUIRichTextEditorStyles) => createStyles({ inheritFontSize: theme?.overrides?.MUIRichTextEditor?.inheritFontSize || { fontSize: "inherit" }, - editor: theme?.overrides?.MUIRichTextEditor?.editor || {}, + editor: theme?.overrides?.MUIRichTextEditor?.editor || { + ...listStyles + }, editorContainer: theme?.overrides?.MUIRichTextEditor?.editorContainer || { margin: theme.spacing(1, 0, 0, 0), cursor: "text", @@ -201,7 +240,6 @@ const styleRenderMap: DraftStyleMap = { } const { hasCommandModifier } = KeyBindingUtil -const autocompleteMinSearchCharCount = 2 const lineHeight = 26 const defaultInlineToolbarControls = ["bold", "italic", "underline", "clear"] @@ -263,10 +301,12 @@ const MUIRichTextEditor: ForwardRefRenderFunction(undefined) const editorStateRef = useRef(editorState) + const [autocompleteItems, setAutocompleteItems] = useState([]) const autocompleteRef = useRef(undefined) const autocompleteSelectionStateRef = useRef(undefined) const autocompletePositionRef = useRef(undefined) const autocompleteLimit = props.autocomplete ? props.autocomplete.suggestLimit || 5 : 5 + const autocompleteMinSearchCharCount = props?.autocomplete?.minSearchCharCount ?? 2; const isFirstFocus = useRef(true) const customBlockMapRef = useRef(undefined) const customStyleMapRef = useRef(undefined) @@ -294,7 +334,10 @@ const MUIRichTextEditor: ForwardRefRenderFunction, placeholder?: string) => { handleInsertAtomicBlockAsync(name, promise, placeholder) - } + }, + insertText: (text: string) => { + handleInsertText(text) + }, })) useEffect(() => { @@ -320,6 +363,8 @@ const MUIRichTextEditor: ForwardRefRenderFunction { if (searchTerm.length < autocompleteMinSearchCharCount) { setSelectedIndex(0) + } else if (autocompleteRef.current?.asyncItems !== undefined) { + autocompleteRef.current?.asyncItems(searchTerm).then((items: TAutocompleteItem[]) => setAutocompleteItems(items)); } }, [searchTerm]) @@ -472,9 +517,13 @@ const MUIRichTextEditor: ForwardRefRenderFunction (item.keys.filter(key => key.includes(searchTerm)).length > 0)) - .splice(0, autocompleteLimit) + if (autocompleteRef.current?.items !== undefined) { + return autocompleteRef.current!.items + .filter(item => (item.keys.filter(key => key.includes(searchTerm)).length > 0)) + .splice(0, autocompleteLimit) + } else { + return autocompleteItems; + } } const handleChange = (state: EditorState) => { @@ -562,6 +611,20 @@ const MUIRichTextEditor: ForwardRefRenderFunction { + const currentContent = editorStateRef.current!.getCurrentContent() + const currentSelection = editorStateRef.current!.getSelection(); + + const newContent = Modifier.replaceText( + currentContent, + currentSelection, + text + ); + + const newEditorState = EditorState.push(editorState, newContent, 'insert-characters'); + handleChange(EditorState.forceSelection(newEditorState, newContent.getSelectionAfter())); + }; + const handleInsertAtomicBlockSync = (name: string, data: any) => { const block = atomicBlockExists(name, props.customControls) if (!block) { @@ -639,6 +702,11 @@ const MUIRichTextEditor: ForwardRefRenderFunction { + const maxDepth = 4; + handleChange(RichUtils.onTab(event, editorState, maxDepth)); + }; + const handleCustomClick = (style: any, id: string) => { if (!props.customControls) { return @@ -1126,6 +1194,7 @@ const MUIRichTextEditor: ForwardRefRenderFunction : null} diff --git a/src/components/ToolbarButton.tsx b/src/components/ToolbarButton.tsx index e958ee2..be82636 100644 --- a/src/components/ToolbarButton.tsx +++ b/src/components/ToolbarButton.tsx @@ -1,5 +1,5 @@ import React, { FunctionComponent } from 'react' -import IconButton from '@mui/material/IconButton' +import {IconButton, Tooltip} from '@mui/material' import { TToolbarComponentProps, TToolbarButtonSize } from './Toolbar' interface IToolbarButtonProps { @@ -34,14 +34,16 @@ const ToolbarButton: FunctionComponent = (props) => { } if (props.icon) { return ( - - {props.icon} - + + + {props.icon} + + ) } if (props.component) { diff --git a/src/components/UrlPopover.tsx b/src/components/UrlPopover.tsx index 0d5b78f..0ce9e98 100644 --- a/src/components/UrlPopover.tsx +++ b/src/components/UrlPopover.tsx @@ -31,6 +31,7 @@ interface IUrlPopoverStateProps extends WithStyles { data?: TUrlData isMedia?: boolean onConfirm: (isMedia?: boolean, ...args: any) => void + onCancel: () => void } const styles = ({ spacing }: Theme) => createStyles({ @@ -69,6 +70,7 @@ const UrlPopover: FunctionComponent = (props) => { return (