Skip to content

Commit

Permalink
fix: refactor editor logic, added data protection to new page
Browse files Browse the repository at this point in the history
  • Loading branch information
web-mi committed Jan 28, 2024
1 parent 051b681 commit a197e39
Show file tree
Hide file tree
Showing 17 changed files with 411 additions and 436 deletions.
1 change: 1 addition & 0 deletions src/api/agency/addAgencyData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function buildAgencyDataRequestBody(consultingTypeResponseId: string | number, f
offline: formData.offline,
demographics: formData.demographics,
counsellingRelations: formData.counsellingRelations,
dataProtection: formData.dataProtection,
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable react/no-children-prop */
import clsx from 'clsx';
import { EditorState, RichUtils } from 'draft-js';
import { RichUtils, SelectionState } from 'draft-js';
import React, { MouseEvent, ReactNode, useCallback, useMemo } from 'react';
import { DraftJsStyleButtonProps } from '@draft-js-plugins/buttons';
import { Button } from 'antd';
Expand All @@ -13,7 +12,7 @@ interface ButtonProps extends DraftJsStyleButtonProps {
interface CreateBlockStyleButtonProps extends Omit<DraftJsStyleButtonProps, 'buttonProps'> {
blockType: string;
children: ReactNode;
editorState: EditorState;
selectionState: SelectionState;
buttonProps?: ButtonProps;
}

Expand All @@ -23,14 +22,15 @@ const BlockStyleButton = ({
theme,
buttonProps,
setEditorState,
editorState,
getEditorState,
selectionState,
}: CreateBlockStyleButtonProps) => {
const toggleStyle = useCallback(
(event: MouseEvent): void => {
event.preventDefault();
setEditorState(RichUtils.toggleBlockType(editorState, blockType));
setEditorState(RichUtils.toggleBlockType(getEditorState(), blockType));
},
[editorState, setEditorState],
[setEditorState, getEditorState],
);

const preventBubblingUp = useCallback((event: MouseEvent): void => {
Expand All @@ -39,27 +39,30 @@ const BlockStyleButton = ({

const blockTypeIsActive = useMemo((): boolean => {
// if the button is rendered before the editor
const editorState = getEditorState();
if (!editorState) {
return false;
}

const type = editorState.getCurrentContent().getBlockForKey(editorState.getSelection().getStartKey()).getType();
const type =
selectionState && editorState.getCurrentContent().getBlockForKey(selectionState.getStartKey()).getType();
return type === blockType;
}, [editorState]);
}, [selectionState, getEditorState]);

const className = blockTypeIsActive ? clsx(theme.button, theme.active) : theme.button;

return (
<Button
children={children}
className={className}
onMouseDown={preventBubblingUp}
onClick={toggleStyle}
size="small"
role="button"
aria-label={`create ${blockType}`}
{...buttonProps}
/>
>
{children}
</Button>
);
};
export default BlockStyleButton;
128 changes: 128 additions & 0 deletions src/components/FormPluginEditor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import DisabledContext from 'antd/es/config-provider/DisabledContext';
import {
ContentState,
convertFromHTML,
DraftEditorCommand,
DraftHandleValue,
EditorState,
getDefaultKeyBinding,
RichUtils,
} from 'draft-js';
import { stateToHTML } from 'draft-js-export-html';
import PluginsEditor from '@draft-js-plugins/editor';
import createPlaceholderPlugin from '../../utils/draftjs/placeholderPlugin';

const Editor = ({
onChange,
value,
onSelectionChange,
onInlineStyleChange,
placeholders,
onBlur,
onFocus,
placeholder,
editorPlugins,
}: any) => {
const disabled = useContext(DisabledContext);

const plugins = useMemo(() => [...editorPlugins, createPlaceholderPlugin({ placeholders })], [placeholders]);

const [editorState, setEditorState] = useState<EditorState>(() => {
const { contentBlocks, entityMap } = convertFromHTML(value);
const contentState = ContentState.createFromBlockArray(contentBlocks, entityMap);
return EditorState.createWithContent(contentState);
});

useEffect(() => {
setEditorState((state) => {
const contentState = state.getCurrentContent();
contentState.getAllEntities().forEach((entity, entityKey) => {
contentState.mergeEntityData(entityKey, { disabled });
});
return EditorState.createWithContent(contentState, state.getDecorator());
});
}, [disabled]);

useEffect(() => {
const resetState = () => {
onSelectionChange(undefined);
onInlineStyleChange(undefined);
};

const selection = editorState.getSelection();
if (!selection.getHasFocus()) return resetState;
onSelectionChange(selection);
onInlineStyleChange(editorState.getCurrentInlineStyle());

return resetState;
}, [editorState, onSelectionChange, onInlineStyleChange]);

const handleChange = useCallback(
(edited: EditorState) => {
setEditorState(edited);
const contentState = edited.getCurrentContent();
onChange(contentState.hasText() ? stateToHTML(contentState) : '');
},
[onChange],
);

// Just because the library isn't properly typed
const extraProps = { onBlur, onFocus };

let classN = `RichEditor-editor`;
const contentState = editorState.getCurrentContent();
if (!contentState.hasText()) {
if (contentState.getBlockMap().first().getType() !== 'unstyled') {
classN += ' RichEditor-hidePlaceholder';
}
}

const handleKeyCommand = useCallback((command: string, editorStat: EditorState): DraftHandleValue => {
const newState = RichUtils.handleKeyCommand(editorStat, command);
if (newState) {
handleChange(newState);
return 'handled';
}
return 'not-handled';
}, []);

const mapKeyToEditorCommand = useCallback(
(e): DraftEditorCommand | null => {
if (e.keyCode === 9) {
const newEditorState = RichUtils.onTab(e, editorState, 4);
if (newEditorState === editorState) {
handleChange(newEditorState);
}
return null;
}
return getDefaultKeyBinding(e);
},
[editorState],
);

const editorRef = useRef<any>();

const focus = useCallback(() => {
editorRef.current.focus();
}, []);

return (
<>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<div className={classN} onClick={focus}>
<PluginsEditor
ref={editorRef}
editorState={editorState}
onChange={handleChange}
placeholder={placeholder}
plugins={plugins}
{...extraProps}
handleKeyCommand={handleKeyCommand}
keyBindingFn={mapKeyToEditorCommand}
/>
</div>
</>
);
};
export default Editor;
147 changes: 147 additions & 0 deletions src/components/FormPluginEditor/FormPluginEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { useContext, useState } from 'react';
import createToolbarPlugin from '@draft-js-plugins/static-toolbar';
import '@draft-js-plugins/static-toolbar/lib/plugin.css';
import {
FormatBold,
FormatItalic,
FormatUnderlined,
FormatListBulleted,
FormatListNumbered,
} from '@mui/icons-material';
import { SelectionState, DraftInlineStyle } from 'draft-js';
import { Form } from 'antd';

import './FormPluginEditor.styles.scss';
import DisabledContext from 'antd/es/config-provider/DisabledContext';
import classNames from 'classnames';
import { FormItemProps } from 'antd/lib/form/FormItem';
import styles from './styles.module.scss';
import createLinkPlugin, { LinkControl } from '../../utils/draftjs/linkPlugin';
import createImagePlugin, { ImageControl } from '../../utils/draftjs/imagePlugin';
import { PlaceholderControl } from '../../utils/draftjs/placeholderPlugin';
import BlockStyleButton from './BlockStyleButton';
import InlineStyleButton from './InlineStyleButton';
import TextStyleSelect from './TextStyleSelect';
import Editor from './Editor';

type FormEditorProps = {
name?: string | string[];
placeholder: string;
disabled?: boolean;
className?: string;
placeholders?: { [key: string]: string };
itemProps: FormItemProps;
};

const FormPluginEditor = ({ placeholder, className, placeholders, itemProps, name }: FormEditorProps) => {
// This state handling for plugins and toolbar is required because multiple editors will collide on the same page
const [{ plugins, Toolbar }] = useState(() => {
const toolbarPlugin = createToolbarPlugin();
const linkPlugin = createLinkPlugin();
const imagePlugin = createImagePlugin();
const { Toolbar: ToolbarComponent } = toolbarPlugin;
return {
plugins: [toolbarPlugin, linkPlugin, imagePlugin],
Toolbar: ToolbarComponent,
};
});

const disabled = useContext(DisabledContext);
const [selectionState, setSelectionState] = useState<SelectionState>();
const [currentInlineStyle, setCurrentInlineStyle] = useState<DraftInlineStyle>();
const [focused, setFocused] = useState(false);

return (
<div
className={classNames('RichEditor-root', className, styles.input, {
[styles.disabled]: disabled,
[styles.focused]: focused,
})}
>
{!disabled && (
<div className="RichEditor-toolbar">
<Toolbar>
{
// may be use React.Fragment instead of div to improve perfomance after React 16
(externalProps) => (
<>
<div className="RichEditor-controls">
<div className="RichEditor-control-group">
<TextStyleSelect {...externalProps} selectionState={selectionState} />
</div>
<div className="RichEditor-control-group">
<InlineStyleButton
{...externalProps}
inlineStyle="BOLD"
active={currentInlineStyle?.has('BOLD')}
>
<FormatBold />
</InlineStyleButton>
<InlineStyleButton
{...externalProps}
inlineStyle="ITALIC"
active={currentInlineStyle?.has('ITALIC')}
>
<FormatItalic />
</InlineStyleButton>
<InlineStyleButton
{...externalProps}
inlineStyle="UNDERLINE"
active={currentInlineStyle?.has('UNDERLINE')}
>
<FormatUnderlined />
</InlineStyleButton>
</div>
<div className="RichEditor-control-group">
<BlockStyleButton
{...externalProps}
selectionState={selectionState}
blockType="unordered-list-item"
>
<FormatListBulleted />
</BlockStyleButton>
<BlockStyleButton
{...externalProps}
selectionState={selectionState}
blockType="ordered-list-item"
>
<FormatListNumbered />
</BlockStyleButton>
</div>
<div className="RichEditor-control-group">
<LinkControl {...externalProps} selectionState={selectionState} />
</div>
<div className="RichEditor-control-group">
<ImageControl {...externalProps} selectionState={selectionState} />
</div>
</div>
{Object.keys(placeholders || {}).length > 0 && (
<PlaceholderControl
placeholders={placeholders}
selectionState={selectionState}
{...externalProps}
/>
)}
</>
)
}
</Toolbar>
</div>
)}

<Form.Item {...itemProps} className={itemProps.className} name={name}>
<Editor
placeholders={placeholders}
onSelectionChange={setSelectionState}
onInlineStyleChange={setCurrentInlineStyle}
placeholder={placeholder}
onBlur={() => setFocused(false)}
onFocus={() => setFocused(true)}
editorPlugins={plugins}
/>
</Form.Item>
</div>
);
};

export default FormPluginEditor;
Loading

0 comments on commit a197e39

Please sign in to comment.