Feature/toast UI editor (#45)
This commit is contained in:
parent
e60e1fa755
commit
3674ee5bd8
@ -25,7 +25,7 @@ module.exports = {
|
|||||||
'react/prop-types': [0],
|
'react/prop-types': [0],
|
||||||
'react/require-default-props': 0,
|
'react/require-default-props': 0,
|
||||||
'import/no-named-as-default': 0,
|
'import/no-named-as-default': 0,
|
||||||
"react/react-in-jsx-scope": "off",
|
'react/react-in-jsx-scope': 'off',
|
||||||
'import/order': [
|
'import/order': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
@ -89,7 +89,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"react/react-in-jsx-scope": "off",
|
'react/react-in-jsx-scope': 'off',
|
||||||
'react/prop-types': [0],
|
'react/prop-types': [0],
|
||||||
'react/require-default-props': 0,
|
'react/require-default-props': 0,
|
||||||
'no-duplicate-imports': [0], // handled by @typescript-eslint
|
'no-duplicate-imports': [0], // handled by @typescript-eslint
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@staticcms/core",
|
"name": "@staticcms/core",
|
||||||
"version": "0.3.7",
|
"version": "1.0.0-alpha1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Static CMS core application.",
|
"description": "Static CMS core application.",
|
||||||
"repository": "https://github.com/StaticJsCMS/static-cms",
|
"repository": "https://github.com/StaticJsCMS/static-cms",
|
||||||
@ -93,7 +93,6 @@
|
|||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mdast-util-definitions": "1.2.5",
|
"mdast-util-definitions": "1.2.5",
|
||||||
"mdast-util-to-string": "1.1.0",
|
"mdast-util-to-string": "1.1.0",
|
||||||
"mime-types": "^2.1.35",
|
|
||||||
"minimatch": "3.0.4",
|
"minimatch": "3.0.4",
|
||||||
"moment": "2.29.4",
|
"moment": "2.29.4",
|
||||||
"node-polyglot": "2.4.2",
|
"node-polyglot": "2.4.2",
|
||||||
@ -170,7 +169,6 @@
|
|||||||
"@types/js-yaml": "4.0.5",
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/jwt-decode": "2.2.1",
|
"@types/jwt-decode": "2.2.1",
|
||||||
"@types/lodash": "4.14.185",
|
"@types/lodash": "4.14.185",
|
||||||
"@types/mime-types": "^2.1.1",
|
|
||||||
"@types/minimatch": "5.1.2",
|
"@types/minimatch": "5.1.2",
|
||||||
"@types/node-fetch": "2.6.2",
|
"@types/node-fetch": "2.6.2",
|
||||||
"@types/react": "18.0.21",
|
"@types/react": "18.0.21",
|
||||||
|
@ -10,7 +10,7 @@ import type { MouseEvent } from 'react';
|
|||||||
import type {
|
import type {
|
||||||
AuthenticationPageProps,
|
AuthenticationPageProps,
|
||||||
AuthenticatorConfig,
|
AuthenticatorConfig,
|
||||||
TranslatedProps
|
TranslatedProps,
|
||||||
} from '../../interface';
|
} from '../../interface';
|
||||||
|
|
||||||
const LoginButtonIcon = styled(Icon)`
|
const LoginButtonIcon = styled(Icon)`
|
||||||
|
@ -195,12 +195,14 @@ const Editor = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasChanged && entryDraft.entry) {
|
if (hasChanged && entryDraft.entry) {
|
||||||
createBackup(entryDraft.entry, collection);
|
createBackup(entryDraft.entry, collection);
|
||||||
|
} else if (localBackup) {
|
||||||
|
deleteBackup();
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
createBackup.flush();
|
createBackup.flush();
|
||||||
};
|
};
|
||||||
}, [collection, createBackup, entryDraft.entry, hasChanged]);
|
}, [collection, createBackup, deleteBackup, entryDraft.entry, hasChanged, localBackup]);
|
||||||
|
|
||||||
const [prevCollection, setPrevCollection] = useState<Collection | null>(null);
|
const [prevCollection, setPrevCollection] = useState<Collection | null>(null);
|
||||||
const [preSlug, setPrevSlug] = useState<string | undefined | null>(null);
|
const [preSlug, setPrevSlug] = useState<string | undefined | null>(null);
|
||||||
|
@ -5,13 +5,12 @@ import { translate } from 'react-polyglot';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addDraftEntryMediaFile as addDraftEntryMediaFileAction,
|
|
||||||
changeDraftField as changeDraftFieldAction,
|
changeDraftField as changeDraftFieldAction,
|
||||||
changeDraftFieldValidation as changeDraftFieldValidationAction,
|
changeDraftFieldValidation as changeDraftFieldValidationAction,
|
||||||
clearFieldErrors as clearFieldErrorsAction,
|
clearFieldErrors as clearFieldErrorsAction,
|
||||||
tryLoadEntry,
|
tryLoadEntry,
|
||||||
} from '../../../actions/entries';
|
} from '../../../actions/entries';
|
||||||
import { addAsset as addAssetAction, getAsset as getAssetAction } from '../../../actions/media';
|
import { getAsset as getAssetAction } from '../../../actions/media';
|
||||||
import {
|
import {
|
||||||
clearMediaControl as clearMediaControlAction,
|
clearMediaControl as clearMediaControlAction,
|
||||||
openMediaLibrary as openMediaLibraryAction,
|
openMediaLibrary as openMediaLibraryAction,
|
||||||
@ -157,8 +156,6 @@ const EditorControl = ({
|
|||||||
locale,
|
locale,
|
||||||
mediaPaths,
|
mediaPaths,
|
||||||
changeDraftFieldValidation,
|
changeDraftFieldValidation,
|
||||||
addAsset,
|
|
||||||
addDraftEntryMediaFile,
|
|
||||||
openMediaLibrary,
|
openMediaLibrary,
|
||||||
parentPath,
|
parentPath,
|
||||||
query,
|
query,
|
||||||
@ -237,8 +234,6 @@ const EditorControl = ({
|
|||||||
mediaPaths,
|
mediaPaths,
|
||||||
onChange: handleChangeDraftField,
|
onChange: handleChangeDraftField,
|
||||||
clearMediaControl,
|
clearMediaControl,
|
||||||
addAsset,
|
|
||||||
addDraftEntryMediaFile,
|
|
||||||
openMediaLibrary,
|
openMediaLibrary,
|
||||||
removeInsertedMedia,
|
removeInsertedMedia,
|
||||||
removeMediaControl,
|
removeMediaControl,
|
||||||
@ -318,8 +313,6 @@ function mapStateToProps(state: RootState, ownProps: EditorControlOwnProps) {
|
|||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
changeDraftField: changeDraftFieldAction,
|
changeDraftField: changeDraftFieldAction,
|
||||||
changeDraftFieldValidation: changeDraftFieldValidationAction,
|
changeDraftFieldValidation: changeDraftFieldValidationAction,
|
||||||
addAsset: addAssetAction,
|
|
||||||
addDraftEntryMediaFile: addDraftEntryMediaFileAction,
|
|
||||||
openMediaLibrary: openMediaLibraryAction,
|
openMediaLibrary: openMediaLibraryAction,
|
||||||
clearMediaControl: clearMediaControlAction,
|
clearMediaControl: clearMediaControlAction,
|
||||||
removeMediaControl: removeMediaControlAction,
|
removeMediaControl: removeMediaControlAction,
|
||||||
|
@ -117,7 +117,9 @@ const ListItemTopBar = ({
|
|||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
) : null}
|
) : null}
|
||||||
<StyledTitle key="title" onClick={onCollapseToggle}>{title}</StyledTitle>
|
<StyledTitle key="title" onClick={onCollapseToggle}>
|
||||||
|
{title}
|
||||||
|
</StyledTitle>
|
||||||
{dragHandleHOC ? <DragHandle dragHandleHOC={dragHandleHOC} /> : null}
|
{dragHandleHOC ? <DragHandle dragHandleHOC={dragHandleHOC} /> : null}
|
||||||
{onRemove ? (
|
{onRemove ? (
|
||||||
<TopBarButton onClick={onRemove}>
|
<TopBarButton onClick={onRemove}>
|
||||||
|
2
src/constants/files.ts
Normal file
2
src/constants/files.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const IMAGE_EXTENSION_REGEX =
|
||||||
|
/(\.apng|\.avif|\.gif|\.jpg|\.jpeg|\.jfif|\.pjpeg|\.pjp|\.png|\.svg|\.webp)$/g;
|
@ -1,20 +1,44 @@
|
|||||||
import { useCallback } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import type { EditorProps } from '@toast-ui/react-editor';
|
import type { ToolbarItemOptions } from '@toast-ui/editor/types/ui';
|
||||||
import type { PluginContext } from '@toast-ui/editor/types/editor';
|
export interface ImagePluginProps {
|
||||||
import type { Field } from '../interface';
|
openMediaLibrary: (forImages: boolean) => void;
|
||||||
|
|
||||||
export interface ShortCodePluginProps {
|
|
||||||
fields: Field;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useShortCodePlugin = (_props: ShortCodePluginProps) => {
|
const PREFIX = 'toastui-editor-';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const plugin: Required<EditorProps>['plugins'][number] = useCallback((_context: PluginContext, _options?: any) => {
|
const useImagePlugin = ({ openMediaLibrary }: ImagePluginProps): ToolbarItemOptions => {
|
||||||
return null;
|
const toolbarButton = useMemo(() => {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'image toastui-editor-toolbar-icons';
|
||||||
|
btn.ariaLabel = 'Insert image';
|
||||||
|
btn.setAttribute('style', 'margin: 0;');
|
||||||
|
return btn;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return plugin;
|
useEffect(() => {
|
||||||
|
const handler = () => {
|
||||||
|
openMediaLibrary(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
toolbarButton.addEventListener('click', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
toolbarButton.removeEventListener('click', handler);
|
||||||
|
};
|
||||||
|
}, [openMediaLibrary, toolbarButton]);
|
||||||
|
|
||||||
|
const toolbarItem: ToolbarItemOptions = useMemo(
|
||||||
|
() => ({
|
||||||
|
name: 'cmsimage',
|
||||||
|
className: `${PREFIX}toolbar-icons color`,
|
||||||
|
el: toolbarButton,
|
||||||
|
}),
|
||||||
|
[toolbarButton],
|
||||||
|
);
|
||||||
|
|
||||||
|
return toolbarItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useShortCodePlugin;
|
export default useImagePlugin;
|
||||||
|
@ -246,8 +246,6 @@ export interface WidgetControlProps<T, F extends Field = Field> {
|
|||||||
locale: string | undefined;
|
locale: string | undefined;
|
||||||
mediaPaths: Record<string, string | string[]>;
|
mediaPaths: Record<string, string | string[]>;
|
||||||
onChange: (value: T | null | undefined) => void;
|
onChange: (value: T | null | undefined) => void;
|
||||||
addAsset: EditorControlProps['addAsset'];
|
|
||||||
addDraftEntryMediaFile: EditorControlProps['addDraftEntryMediaFile'];
|
|
||||||
clearMediaControl: EditorControlProps['clearMediaControl'];
|
clearMediaControl: EditorControlProps['clearMediaControl'];
|
||||||
openMediaLibrary: EditorControlProps['openMediaLibrary'];
|
openMediaLibrary: EditorControlProps['openMediaLibrary'];
|
||||||
removeInsertedMedia: EditorControlProps['removeInsertedMedia'];
|
removeInsertedMedia: EditorControlProps['removeInsertedMedia'];
|
||||||
|
12
src/lib/util/fetch.util.ts
Normal file
12
src/lib/util/fetch.util.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export async function doesUrlFileExist(url: string): Promise<{ type: string; exists: boolean }> {
|
||||||
|
const cleanUrl = url.replace(/^blob:/g, '');
|
||||||
|
|
||||||
|
const baseUrl = `${window.location.protocol}//${window.location.host}/`;
|
||||||
|
|
||||||
|
if (!cleanUrl.startsWith('/') && !cleanUrl.startsWith(baseUrl)) {
|
||||||
|
return { type: 'Unknown', exists: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(cleanUrl, { method: 'HEAD' });
|
||||||
|
return { type: response.headers.get('Content-Type') ?? 'text', exists: response.ok };
|
||||||
|
}
|
@ -8,7 +8,7 @@ import type {
|
|||||||
FieldValidationMethod,
|
FieldValidationMethod,
|
||||||
FieldValidationMethodProps,
|
FieldValidationMethodProps,
|
||||||
ValueOrNestedValue,
|
ValueOrNestedValue,
|
||||||
Widget
|
Widget,
|
||||||
} from '../../interface';
|
} from '../../interface';
|
||||||
|
|
||||||
export function isEmpty(value: ValueOrNestedValue) {
|
export function isEmpty(value: ValueOrNestedValue) {
|
||||||
|
@ -9,9 +9,11 @@ type MessageType = 'error' | 'warning' | 'info' | 'success';
|
|||||||
export interface SnackbarMessage {
|
export interface SnackbarMessage {
|
||||||
id: string;
|
id: string;
|
||||||
type: MessageType;
|
type: MessageType;
|
||||||
message: string | {
|
message:
|
||||||
key: string;
|
| string
|
||||||
} & Record<string, unknown>;
|
| ({
|
||||||
|
key: string;
|
||||||
|
} & Record<string, unknown>);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define a type for the slice state
|
// Define a type for the slice state
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { Editor } from '@toast-ui/react-editor';
|
import { Editor } from '@toast-ui/react-editor';
|
||||||
import mime from 'mime-types';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
|
|
||||||
import FieldLabel from '../../components/UI/FieldLabel';
|
import FieldLabel from '../../components/UI/FieldLabel';
|
||||||
import Outline from '../../components/UI/Outline';
|
import Outline from '../../components/UI/Outline';
|
||||||
import { sanitizeSlug } from '../../lib/urlHelper';
|
import { IMAGE_EXTENSION_REGEX } from '../../constants/files';
|
||||||
import { selectMediaFilePath } from '../../lib/util/media.util';
|
import useImagePlugin from '../../editor-components/editorPlugin';
|
||||||
import { createAssetProxy } from '../../valueObjects/AssetProxy';
|
import { doesUrlFileExist } from '../../lib/util/fetch.util';
|
||||||
|
import { isNotNullish } from '../../lib/util/null.util';
|
||||||
|
import { isNotEmpty } from '../../lib/util/string.util';
|
||||||
|
|
||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import type { MarkdownField, WidgetControlProps } from '../../interface';
|
import type { MarkdownField, MediaLibrary, WidgetControlProps } from '../../interface';
|
||||||
|
|
||||||
import '@toast-ui/editor/dist/toastui-editor.css';
|
import '@toast-ui/editor/dist/toastui-editor.css';
|
||||||
|
|
||||||
|
const imageFilePattern = /(!)?\[([^\]]*)\]\(([^)]+)\)/;
|
||||||
|
|
||||||
const StyledEditorWrapper = styled('div')`
|
const StyledEditorWrapper = styled('div')`
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -42,11 +46,9 @@ const MarkdownControl = ({
|
|||||||
onChange,
|
onChange,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
field,
|
field,
|
||||||
addAsset,
|
openMediaLibrary,
|
||||||
addDraftEntryMediaFile,
|
mediaPaths,
|
||||||
config,
|
getAsset,
|
||||||
collection,
|
|
||||||
entry,
|
|
||||||
}: WidgetControlProps<string, MarkdownField>) => {
|
}: WidgetControlProps<string, MarkdownField>) => {
|
||||||
const [internalValue, setInternalValue] = useState(value ?? '');
|
const [internalValue, setInternalValue] = useState(value ?? '');
|
||||||
const editorRef = useMemo(() => React.createRef(), []) as RefObject<Editor>;
|
const editorRef = useMemo(() => React.createRef(), []) as RefObject<Editor>;
|
||||||
@ -70,43 +72,88 @@ const MarkdownControl = ({
|
|||||||
editorRef.current?.getInstance().focus();
|
editorRef.current?.getInstance().focus();
|
||||||
}, [editorRef]);
|
}, [editorRef]);
|
||||||
|
|
||||||
const imageUpload = useCallback(
|
const controlID: string = useMemo(() => uuid(), []);
|
||||||
(blob: Blob | File, callback: (url: string, text?: string) => void) => {
|
const mediaLibraryFieldOptions: MediaLibrary = useMemo(
|
||||||
let file: File;
|
() => field.media_library ?? {},
|
||||||
if (blob instanceof Blob) {
|
[field.media_library],
|
||||||
blob.type;
|
|
||||||
file = new File([blob], `${uuid()}.${mime.extension(blob.type)}`);
|
|
||||||
} else {
|
|
||||||
file = blob;
|
|
||||||
}
|
|
||||||
const fileName = sanitizeSlug(file.name.toLowerCase(), config.slug);
|
|
||||||
const path = selectMediaFilePath(config, collection, entry, fileName, field);
|
|
||||||
const blobUrl = URL.createObjectURL(file);
|
|
||||||
addAsset(
|
|
||||||
createAssetProxy({
|
|
||||||
url: blobUrl,
|
|
||||||
file,
|
|
||||||
path,
|
|
||||||
field,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
addDraftEntryMediaFile({
|
|
||||||
name: file.name,
|
|
||||||
id: file.name,
|
|
||||||
size: file.size,
|
|
||||||
displayURL: blobUrl,
|
|
||||||
path,
|
|
||||||
draft: true,
|
|
||||||
url: blobUrl,
|
|
||||||
file,
|
|
||||||
field,
|
|
||||||
});
|
|
||||||
console.log(blob);
|
|
||||||
callback(path);
|
|
||||||
handleOnChange();
|
|
||||||
},
|
|
||||||
[addAsset, addDraftEntryMediaFile, collection, config, entry, field, handleOnChange],
|
|
||||||
);
|
);
|
||||||
|
const handleOpenMedialLibrary = useCallback(
|
||||||
|
(forImage: boolean) => {
|
||||||
|
openMediaLibrary({
|
||||||
|
controlID,
|
||||||
|
forImage,
|
||||||
|
privateUpload: false,
|
||||||
|
allowMultiple: false,
|
||||||
|
field,
|
||||||
|
config: 'config' in mediaLibraryFieldOptions ? mediaLibraryFieldOptions.config : undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[controlID, field, mediaLibraryFieldOptions, openMediaLibrary],
|
||||||
|
);
|
||||||
|
|
||||||
|
const imageToolbarButton = useImagePlugin({
|
||||||
|
openMediaLibrary: handleOpenMedialLibrary,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMedia = useCallback(
|
||||||
|
async (path: string) => {
|
||||||
|
const { type, exists } = await doesUrlFileExist(path);
|
||||||
|
if (!exists) {
|
||||||
|
const asset = getAsset(path, field);
|
||||||
|
if (isNotNullish(asset)) {
|
||||||
|
return {
|
||||||
|
type: IMAGE_EXTENSION_REGEX.test(path) ? 'image' : 'file',
|
||||||
|
exists: false,
|
||||||
|
url: asset.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { url: path, type, exists };
|
||||||
|
},
|
||||||
|
[field, getAsset],
|
||||||
|
);
|
||||||
|
|
||||||
|
const mediaPath = mediaPaths[controlID];
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEmpty(mediaPath) || Array.isArray(mediaPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addMedia = async () => {
|
||||||
|
const { type } = await getMedia(mediaPath);
|
||||||
|
let content: string | undefined;
|
||||||
|
const name = mediaPath.split('/').pop();
|
||||||
|
if (type.startsWith('image')) {
|
||||||
|
content = `![${name}](${mediaPath})`;
|
||||||
|
} else {
|
||||||
|
content = `[${name}](${mediaPath})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNotEmpty(content)) {
|
||||||
|
const editorInstance = editorRef.current?.getInstance();
|
||||||
|
if (!editorInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editorInstance.focus();
|
||||||
|
const isOnMarkdown = editorInstance.isMarkdownMode();
|
||||||
|
if (!isOnMarkdown) {
|
||||||
|
editorInstance.changeMode('markdown');
|
||||||
|
}
|
||||||
|
editorInstance.insertText(content);
|
||||||
|
if (!isOnMarkdown) {
|
||||||
|
editorInstance.changeMode('wysiwyg');
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
handleOnChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addMedia();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [field, mediaPath]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledEditorWrapper key="markdown-control-wrapper">
|
<StyledEditorWrapper key="markdown-control-wrapper">
|
||||||
@ -130,16 +177,42 @@ const MarkdownControl = ({
|
|||||||
['heading', 'bold', 'italic', 'strike'],
|
['heading', 'bold', 'italic', 'strike'],
|
||||||
['hr', 'quote'],
|
['hr', 'quote'],
|
||||||
['ul', 'ol', 'task', 'indent', 'outdent'],
|
['ul', 'ol', 'task', 'indent', 'outdent'],
|
||||||
['table', 'image', 'link'],
|
['table', imageToolbarButton, 'link'],
|
||||||
['code', 'codeblock'],
|
['code', 'codeblock'],
|
||||||
]}
|
]}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
onFocus={handleOnFocus}
|
onFocus={handleOnFocus}
|
||||||
onBlur={handleOnBlur}
|
onBlur={handleOnBlur}
|
||||||
hooks={{
|
|
||||||
addImageBlobHook: imageUpload,
|
|
||||||
}}
|
|
||||||
autofocus={false}
|
autofocus={false}
|
||||||
|
widgetRules={[
|
||||||
|
{
|
||||||
|
rule: imageFilePattern,
|
||||||
|
toDOM(text) {
|
||||||
|
const rule = imageFilePattern;
|
||||||
|
const matched = text.match(rule);
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
if (matched?.length === 4) {
|
||||||
|
// Image
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.setAttribute('src', getAsset(matched[3], field).url);
|
||||||
|
img.setAttribute('style', 'width: 100%;');
|
||||||
|
img.innerHTML = 'test';
|
||||||
|
return img;
|
||||||
|
} else {
|
||||||
|
// File
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute('target', '_blank');
|
||||||
|
a.setAttribute('href', matched[2]);
|
||||||
|
a.innerHTML = matched[1];
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.createElement('div');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<Outline key="markdown-control-outline" hasLabel hasError={hasErrors} />
|
<Outline key="markdown-control-outline" hasLabel hasError={hasErrors} />
|
||||||
</StyledEditorWrapper>
|
</StyledEditorWrapper>
|
||||||
|
@ -39,20 +39,15 @@ const SelectControl = ({
|
|||||||
const isEmpty =
|
const isEmpty =
|
||||||
isMultiple && Array.isArray(selectedOption) ? !selectedOption?.length : !selectedOption;
|
isMultiple && Array.isArray(selectedOption) ? !selectedOption?.length : !selectedOption;
|
||||||
|
|
||||||
console.log('[value]', selectedOption, field.required, isEmpty, isMultiple);
|
|
||||||
|
|
||||||
if (field.required && isEmpty && isMultiple) {
|
if (field.required && isEmpty && isMultiple) {
|
||||||
setInternalValue([]);
|
setInternalValue([]);
|
||||||
onChange([]);
|
onChange([]);
|
||||||
console.log('emtpy array!');
|
|
||||||
} else if (isEmpty) {
|
} else if (isEmpty) {
|
||||||
setInternalValue('');
|
setInternalValue('');
|
||||||
onChange('');
|
onChange('');
|
||||||
console.log('emtpy string!');
|
|
||||||
} else if (typeof selectedOption === 'string' || isMultiple) {
|
} else if (typeof selectedOption === 'string' || isMultiple) {
|
||||||
setInternalValue(selectedOption);
|
setInternalValue(selectedOption);
|
||||||
onChange(selectedOption);
|
onChange(selectedOption);
|
||||||
console.log('valid value!', selectedOption);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[field, onChange],
|
[field, onChange],
|
||||||
@ -85,8 +80,8 @@ const SelectControl = ({
|
|||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||||
{selectValues.map(selectValue => {
|
{selectValues.map(selectValue => {
|
||||||
const label = optionsByValue[selectValue]?.label ?? selectValue;
|
const label = optionsByValue[selectValue]?.label ?? selectValue;
|
||||||
return <Chip key={selectValue} label={label} />
|
return <Chip key={selectValue} label={label} />;
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
selectValues
|
selectValues
|
||||||
|
@ -4,7 +4,12 @@ import React, { useCallback, useState } from 'react';
|
|||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import type { StringOrTextField, WidgetControlProps } from '../../interface';
|
import type { StringOrTextField, WidgetControlProps } from '../../interface';
|
||||||
|
|
||||||
const TextControl = ({ label, value, onChange, hasErrors }: WidgetControlProps<string, StringOrTextField>) => {
|
const TextControl = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
hasErrors,
|
||||||
|
}: WidgetControlProps<string, StringOrTextField>) => {
|
||||||
const [internalValue, setInternalValue] = useState(value ?? '');
|
const [internalValue, setInternalValue] = useState(value ?? '');
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
|
@ -7,7 +7,10 @@ class GitHubStarButton extends PureComponent {
|
|||||||
href="https://github.com/StaticJsCMS/static-cms"
|
href="https://github.com/StaticJsCMS/static-cms"
|
||||||
aria-label="Star StaticJsCMS/static-cms on GitHub"
|
aria-label="Star StaticJsCMS/static-cms on GitHub"
|
||||||
>
|
>
|
||||||
<img alt="Star StaticJsCMS/static-cms on GitHub" src="https://img.shields.io/github/stars/StaticJsCMS/static-cms?style=social" />
|
<img
|
||||||
|
alt="Star StaticJsCMS/static-cms on GitHub"
|
||||||
|
src="https://img.shields.io/github/stars/StaticJsCMS/static-cms?style=social"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2250,11 +2250,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/unist" "*"
|
"@types/unist" "*"
|
||||||
|
|
||||||
"@types/mime-types@^2.1.1":
|
|
||||||
version "2.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.1.tgz#d9ba43490fa3a3df958759adf69396c3532cf2c1"
|
|
||||||
integrity sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==
|
|
||||||
|
|
||||||
"@types/mime@*":
|
"@types/mime@*":
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
|
||||||
@ -7193,7 +7188,7 @@ mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
|
|||||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||||
|
|
||||||
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@^2.1.35, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
|
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||||
version "2.1.35"
|
version "2.1.35"
|
||||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user