Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UE-385 :: 1 time review of mui rte upstream prs #22

Merged
merged 9 commits into from
Apr 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions examples/autocomplete-async/index.tsx
Original file line number Diff line number Diff line change
@@ -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<TStaff> = (props) => {
return (
<>
<ListItemAvatar>
<Avatar src={props.avatar}>{props.name.substr(0, 1)}</Avatar>
</ListItemAvatar>
<ListItemText
primary={props.name}
/>
</>
)
}

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<TAutocompleteItem[]> => {
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: <Staff name={`${u.first_name} ${u.last_name}`} avatar={u.avatar} />,
}});
};

const Autocomplete = () => {
return (
<MUIRichTextEditor
label="Try typing ':grin' or '/mexico'..."
onSave={save}
autocomplete={{
strategies: [
{
items: emojis,
triggerChar: ":"
},
{
items: cities,
triggerChar: "/"
},
{
asyncItems: searchUsers,
triggerChar: "@",
insertSpaceAfter: false
}
]
}}
/>
)
}

export default Autocomplete
1 change: 1 addition & 0 deletions examples/autocomplete/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const Autocomplete = () => {
label="Try typing ':grin' or '/mexico'..."
onSave={save}
autocomplete={{
minSearchCharCount: 2,
strategies: [
{
items: emojis,
Expand Down
2 changes: 2 additions & 0 deletions examples/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -51,6 +52,7 @@ const App = () => {
<button onClick={() => setSample(<KeyBindings />)}>Key Bindings</button>
<button onClick={() => setSample(<MaxLength />)}>Max length</button>
<button onClick={() => setSample(<Autocomplete />)}>Autocomplete</button>
<button onClick={() => setSample(<AutocompleteAsync />)}>AutocompleteAsync</button>
<button onClick={() => setSample(<AutocompleteAtomic />)}>Autocomplete Atomic</button>
<div style={{
margin: "20px 0"
Expand Down
5 changes: 4 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,14 @@ export declare type TDecorator = {
regex: RegExp;
};
export declare type TAutocompleteStrategy = {
asyncItems?: (search: string) => Promise<TAutocompleteItem[]>
triggerChar: string;
items: TAutocompleteItem[];
items?: TAutocompleteItem[];
insertSpaceAfter?: boolean;
atomicBlockName?: string;
};
export declare type TAutocomplete = {
minSearchCharCount?: number;
strategies: TAutocompleteStrategy[];
suggestLimit?: number;
};
Expand All @@ -96,6 +98,7 @@ export declare type TAsyncAtomicBlockResponse = {
export declare type TMUIRichTextEditorRef = {
focus: () => void;
save: () => void;
insertText: (text: string) => void;
/**
* @deprecated Use `insertAtomicBlockSync` instead.
*/
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export {default} from './dist/MUIRichTextEditor'
module.exports = require('./dist/MUIRichTextEditor')
84 changes: 77 additions & 7 deletions src/MUIRichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ export type TDecorator = {
}

export type TAutocompleteStrategy = {
asyncItems?: (search: string) => Promise<TAutocompleteItem[]>
triggerChar: string
items: TAutocompleteItem[]
items?: TAutocompleteItem[]
insertSpaceAfter?: boolean
atomicBlockName?: string
}

export type TAutocomplete = {
minSearchCharCount?: number;
strategies: TAutocompleteStrategy[]
suggestLimit?: number
}
Expand Down Expand Up @@ -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 || {
Expand All @@ -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",
Expand Down Expand Up @@ -201,7 +240,6 @@ const styleRenderMap: DraftStyleMap = {
}

const { hasCommandModifier } = KeyBindingUtil
const autocompleteMinSearchCharCount = 2
const lineHeight = 26
const defaultInlineToolbarControls = ["bold", "italic", "underline", "clear"]

Expand Down Expand Up @@ -263,10 +301,12 @@ const MUIRichTextEditor: ForwardRefRenderFunction<TMUIRichTextEditorRef, IMUIRic
const editorId = props.id || "mui-rte"
const toolbarPositionRef = useRef<TPosition | undefined>(undefined)
const editorStateRef = useRef<EditorState | null>(editorState)
const [autocompleteItems, setAutocompleteItems] = useState<TAutocompleteItem[]>([])
const autocompleteRef = useRef<TAutocompleteStrategy | undefined>(undefined)
const autocompleteSelectionStateRef = useRef<SelectionState | undefined>(undefined)
const autocompletePositionRef = useRef<TPosition | undefined>(undefined)
const autocompleteLimit = props.autocomplete ? props.autocomplete.suggestLimit || 5 : 5
const autocompleteMinSearchCharCount = props?.autocomplete?.minSearchCharCount ?? 2;
const isFirstFocus = useRef(true)
const customBlockMapRef = useRef<DraftBlockRenderMap | undefined>(undefined)
const customStyleMapRef = useRef<DraftStyleMap | undefined>(undefined)
Expand Down Expand Up @@ -294,7 +334,10 @@ const MUIRichTextEditor: ForwardRefRenderFunction<TMUIRichTextEditorRef, IMUIRic
},
insertAtomicBlockAsync: (name: string, promise: Promise<TAsyncAtomicBlockResponse>, placeholder?: string) => {
handleInsertAtomicBlockAsync(name, promise, placeholder)
}
},
insertText: (text: string) => {
handleInsertText(text)
},
}))

useEffect(() => {
Expand All @@ -320,6 +363,8 @@ const MUIRichTextEditor: ForwardRefRenderFunction<TMUIRichTextEditorRef, IMUIRic
useEffect(() => {
if (searchTerm.length < autocompleteMinSearchCharCount) {
setSelectedIndex(0)
} else if (autocompleteRef.current?.asyncItems !== undefined) {
autocompleteRef.current?.asyncItems(searchTerm).then((items: TAutocompleteItem[]) => setAutocompleteItems(items));
}
}, [searchTerm])

Expand Down Expand Up @@ -472,9 +517,13 @@ const MUIRichTextEditor: ForwardRefRenderFunction<TMUIRichTextEditorRef, IMUIRic
if (searchTerm.length < autocompleteMinSearchCharCount) {
return []
}
return autocompleteRef.current!.items
.filter(item => (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) => {
Expand Down Expand Up @@ -562,6 +611,20 @@ const MUIRichTextEditor: ForwardRefRenderFunction<TMUIRichTextEditorRef, IMUIRic
}
}

const handleInsertText = (text: string) => {
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) {
Expand Down Expand Up @@ -639,6 +702,11 @@ const MUIRichTextEditor: ForwardRefRenderFunction<TMUIRichTextEditorRef, IMUIRic
return "not-handled"
}

const onTab = (event: any) => {
const maxDepth = 4;
handleChange(RichUtils.onTab(event, editorState, maxDepth));
};

const handleCustomClick = (style: any, id: string) => {
if (!props.customControls) {
return
Expand Down Expand Up @@ -1126,6 +1194,7 @@ const MUIRichTextEditor: ForwardRefRenderFunction<TMUIRichTextEditorRef, IMUIRic
editorState={editorState}
onChange={handleChange}
onFocus={handleEditorFocus}
onTab={onTab}
readOnly={props.readOnly}
handleKeyCommand={handleKeyCommand}
handleBeforeInput={handleBeforeInput}
Expand All @@ -1142,6 +1211,7 @@ const MUIRichTextEditor: ForwardRefRenderFunction<TMUIRichTextEditorRef, IMUIRic
data={state.urlData}
anchor={state.anchorUrlPopover}
onConfirm={handleConfirmPrompt}
onCancel={dismissPopover}
isMedia={state.urlIsMedia}
/>
: null}
Expand Down
20 changes: 11 additions & 9 deletions src/components/ToolbarButton.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -34,14 +34,16 @@ const ToolbarButton: FunctionComponent<IToolbarButtonProps> = (props) => {
}
if (props.icon) {
return (
<IconButton
{...sharedProps}
aria-label={props.label}
color={props.active ? "primary" : "default"}
size={size}
>
{props.icon}
</IconButton>
<Tooltip title={props.label}>
<IconButton
{...sharedProps}
aria-label={props.label}
color={props.active ? "primary" : "default"}
size={size}
>
{props.icon}
</IconButton>
</Tooltip>
)
}
if (props.component) {
Expand Down
Loading