feat: add raw markdown editor (#866)

This commit is contained in:
Daniel Lautzenheiser 2023-09-06 16:59:27 -04:00 committed by GitHub
parent dbf007a586
commit f37952a84a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 118 additions and 32 deletions

View File

@ -694,6 +694,10 @@ collections:
label: Required With Default
widget: markdown
default: Default **markdown** value
- name: raw
label: Raw Editor
widget: markdown
show_raw: true
- name: pattern
label: Pattern Validation
widget: markdown

View File

@ -6,6 +6,8 @@ import type { ChangeEventHandler, RefObject } from 'react';
export interface TextAreaProps {
value: string;
disabled?: boolean;
placeholder?: string;
className?: string;
'data-testid'?: string;
onChange: ChangeEventHandler<HTMLInputElement>;
}
@ -18,7 +20,7 @@ function getHeight(rawHeight: string): number {
}
const TextArea = forwardRef<HTMLInputElement | null, TextAreaProps>(
({ value, disabled, 'data-testid': dataTestId, onChange }, ref) => {
({ value, disabled, placeholder, className, 'data-testid': dataTestId, onChange }, ref) => {
const [lastAutogrowHeight, setLastAutogrowHeight] = useState(MIN_TEXT_AREA_HEIGHT);
const autoGrow = useCallback(() => {
@ -63,10 +65,12 @@ const TextArea = forwardRef<HTMLInputElement | null, TextAreaProps>(
className: `
flex
w-full
${className}
`,
},
input: {
ref,
placeholder,
className: `
w-full
min-h-[80px]

View File

@ -769,6 +769,7 @@ export interface MarkdownField extends MediaField {
widget: 'markdown';
toolbar_buttons?: MarkdownFieldToolbarButtons;
default?: string;
show_raw?: string;
}
export interface NumberField extends BaseField {

View File

@ -144,6 +144,7 @@ const en: LocalePhrasesRoot = {
addComponent: 'Add Component',
richText: 'Rich Text',
markdown: 'Markdown',
type: 'Type...',
},
image: {
choose: 'Choose an image',

View File

@ -53,6 +53,7 @@ import { StyledLeaf } from '@udecode/plate-styled-components';
import React, { useMemo, useRef } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslate } from 'react-polyglot';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
import { CodeBlockElement, withShortcodeElement } from './components';
@ -104,6 +105,7 @@ import type {
} from '@staticcms/core/interface';
import type { AnyObject, AutoformatPlugin, PlatePlugin } from '@udecode/plate';
import type { FC } from 'react';
import type { t as T } from 'react-polyglot';
import type { MdEditor, MdValue } from './plateTypes';
export interface PlateEditorProps {
@ -129,6 +131,8 @@ const PlateEditor: FC<PlateEditorProps> = ({
onFocus,
onBlur,
}) => {
const t = useTranslate() as T;
const editorContainerRef = useRef<HTMLDivElement | null>(null);
const innerEditorContainerRef = useRef<HTMLDivElement | null>(null);
@ -278,6 +282,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
id={id}
editableProps={{
...editableProps,
placeholder: t('editor.editorWidgets.markdown.type'),
onFocus,
onBlur,
className: '!outline-none',

View File

@ -5,7 +5,6 @@ const editableProps: TEditableProps<MdValue> = {
spellCheck: false,
autoFocus: false,
readOnly: false,
placeholder: 'Type…',
};
export default editableProps;

View File

@ -14,6 +14,7 @@ import type { MdValue } from '../plateTypes';
export interface UseMarkdownToSlateOptions {
shortcodeConfigs?: Record<string, ShortcodeConfig>;
useMdx: boolean;
unload?: boolean;
}
export const markdownToSlate = async (
@ -45,12 +46,17 @@ const useMarkdownToSlate = (
const [slateValue, setSlateValue] = useState<MdValue>([]);
useEffect(() => {
if (loaded && !options.unload) {
return;
}
markdownToSlate(markdownValue, options).then(value => {
setSlateValue(value);
setLoaded(true);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [markdownValue]);
return [
slateValue.length > 0 ? slateValue : [{ type: ELEMENT_PARAGRAPH, children: [{ text: '' }] }],

View File

@ -106,6 +106,7 @@ export default {
public_folder: { type: 'string' },
choose_url: { type: 'boolean' },
multiple: { type: 'boolean' },
show_raw: { type: 'boolean' },
toolbar_buttons: {
type: 'object',
properties: {

View File

@ -1,13 +1,15 @@
import React, { useCallback, useMemo, useState } from 'react';
import Button from '@staticcms/core/components/common/button/Button';
import Field from '@staticcms/core/components/common/field/Field';
import TextArea from '@staticcms/core/components/common/text-field/TextArea';
import useDebounce from '../../lib/hooks/useDebounce';
import useMarkdownToSlate from './plate/hooks/useMarkdownToSlate';
import PlateEditor from './plate/PlateEditor';
import useMarkdownToSlate from './plate/hooks/useMarkdownToSlate';
import serializeMarkdown from './plate/serialization/serializeMarkdown';
import type { MarkdownField, WidgetControlProps } from '@staticcms/core/interface';
import type { FC } from 'react';
import type { ChangeEvent, FC } from 'react';
import type { MdValue } from './plate/plateTypes';
export interface WithMarkdownControlProps {
@ -28,6 +30,7 @@ const withMarkdownControl = ({ useMdx }: WithMarkdownControlProps) => {
errors,
forSingleList,
disabled,
t,
} = controlProps;
const [internalRawValue, setInternalValue] = useState(value ?? '');
@ -38,6 +41,8 @@ const withMarkdownControl = ({ useMdx }: WithMarkdownControlProps) => {
const [hasFocus, setHasFocus] = useState(false);
const debouncedFocus = useDebounce(hasFocus, 150);
const [showRaw, setShowRaw] = useState(false);
const handleOnFocus = useCallback(() => {
setHasFocus(true);
}, []);
@ -57,24 +62,40 @@ const withMarkdownControl = ({ useMdx }: WithMarkdownControlProps) => {
[internalValue, onChange],
);
const handleRawOnChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const rawValue = event.target.value;
if (rawValue !== internalValue) {
setInternalValue(rawValue);
onChange(rawValue);
}
},
[internalValue, onChange],
);
const handleLabelClick = useCallback(() => {
// editorRef.current?.getInstance().focus();
}, []);
const [slateValue, loaded] = useMarkdownToSlate(internalValue, { useMdx });
const handleShowRaw = useCallback(() => {
if (!field.show_raw) {
return;
}
return useMemo(
() => (
<Field
label={label}
errors={errors}
forSingleList={forSingleList}
hint={field.hint}
noHightlight
disabled={disabled}
>
{loaded ? (
setShowRaw(true);
}, [field.show_raw]);
const handleShowRich = useCallback(() => {
setShowRaw(false);
}, []);
const [slateValue, loaded] = useMarkdownToSlate(internalValue, { useMdx, unload: showRaw });
const richEditor = useMemo(
() =>
loaded ? (
<PlateEditor
key="plate-editor"
initialValue={slateValue}
collection={collection}
entry={entry}
@ -85,9 +106,7 @@ const withMarkdownControl = ({ useMdx }: WithMarkdownControlProps) => {
onFocus={handleOnFocus}
onBlur={handleOnBlur}
/>
) : null}
</Field>
),
) : null,
// eslint-disable-next-line react-hooks/exhaustive-deps
[
collection,
@ -100,11 +119,57 @@ const withMarkdownControl = ({ useMdx }: WithMarkdownControlProps) => {
handleOnFocus,
hasErrors,
hasFocus,
label,
loaded,
slateValue,
showRaw,
],
);
return (
<Field
label={label}
errors={errors}
forSingleList={forSingleList}
hint={field.hint}
noHightlight
disabled={disabled}
>
{showRaw ? (
<TextArea
key="raw-editor"
value={internalValue}
disabled={disabled}
onChange={handleRawOnChange}
placeholder={t('editor.editorWidgets.markdown.type')}
className="mt-2"
/>
) : (
richEditor
)}
{field.show_raw ? (
<div className="px-3 mt-2 flex gap-2">
<Button
data-testid="rich-editor"
size="small"
variant={!showRaw ? 'contained' : 'outlined'}
onClick={handleShowRich}
disabled={disabled}
>
{t('editor.editorWidgets.markdown.richText')}
</Button>
<Button
data-testid="rich-editor"
size="small"
variant={showRaw ? 'contained' : 'outlined'}
onClick={handleShowRaw}
disabled={disabled}
>
{t('editor.editorWidgets.markdown.markdown')}
</Button>
</div>
) : null}
</Field>
);
};
return MarkdownControl;