feat: add raw markdown editor (#866)
This commit is contained in:
parent
dbf007a586
commit
f37952a84a
@ -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
|
||||
|
@ -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]
|
||||
|
@ -769,6 +769,7 @@ export interface MarkdownField extends MediaField {
|
||||
widget: 'markdown';
|
||||
toolbar_buttons?: MarkdownFieldToolbarButtons;
|
||||
default?: string;
|
||||
show_raw?: string;
|
||||
}
|
||||
|
||||
export interface NumberField extends BaseField {
|
||||
|
@ -144,6 +144,7 @@ const en: LocalePhrasesRoot = {
|
||||
addComponent: 'Add Component',
|
||||
richText: 'Rich Text',
|
||||
markdown: 'Markdown',
|
||||
type: 'Type...',
|
||||
},
|
||||
image: {
|
||||
choose: 'Choose an image',
|
||||
|
@ -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',
|
||||
|
@ -5,7 +5,6 @@ const editableProps: TEditableProps<MdValue> = {
|
||||
spellCheck: false,
|
||||
autoFocus: false,
|
||||
readOnly: false,
|
||||
placeholder: 'Type…',
|
||||
};
|
||||
|
||||
export default editableProps;
|
||||
|
@ -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: '' }] }],
|
||||
|
@ -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: {
|
||||
|
@ -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,37 +62,51 @@ 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 ? (
|
||||
<PlateEditor
|
||||
initialValue={slateValue}
|
||||
collection={collection}
|
||||
entry={entry}
|
||||
field={field}
|
||||
useMdx={useMdx}
|
||||
controlProps={controlProps}
|
||||
onChange={handleOnChange}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
/>
|
||||
) : null}
|
||||
</Field>
|
||||
),
|
||||
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}
|
||||
field={field}
|
||||
useMdx={useMdx}
|
||||
controlProps={controlProps}
|
||||
onChange={handleOnChange}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
/>
|
||||
) : 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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user