fix: media popup (#225)
This commit is contained in:
parent
c1ae2a1bcc
commit
6602f37495
@ -56,7 +56,7 @@ collections:
|
|||||||
required: false
|
required: false
|
||||||
- label: Body
|
- label: Body
|
||||||
name: body
|
name: body
|
||||||
widget: markdown
|
widget: mdx
|
||||||
hint: Main content goes here.
|
hint: Main content goes here.
|
||||||
- name: faq
|
- name: faq
|
||||||
label: FAQ
|
label: FAQ
|
||||||
|
@ -14,4 +14,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
setupFiles: ['./test/setupEnv.js'],
|
setupFiles: ['./test/setupEnv.js'],
|
||||||
testRegex: '\\.spec\\.tsx?$',
|
testRegex: '\\.spec\\.tsx?$',
|
||||||
|
snapshotSerializers: ['@emotion/jest/serializer'],
|
||||||
};
|
};
|
||||||
|
@ -176,8 +176,11 @@
|
|||||||
"@babel/preset-react": "7.18.6",
|
"@babel/preset-react": "7.18.6",
|
||||||
"@babel/preset-typescript": "7.18.6",
|
"@babel/preset-typescript": "7.18.6",
|
||||||
"@emotion/eslint-plugin": "11.10.0",
|
"@emotion/eslint-plugin": "11.10.0",
|
||||||
|
"@emotion/jest": "11.10.5",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.10",
|
"@pmmmwh/react-refresh-webpack-plugin": "0.5.10",
|
||||||
"@simbathesailor/use-what-changed": "2.0.0",
|
"@simbathesailor/use-what-changed": "2.0.0",
|
||||||
|
"@testing-library/jest-dom": "5.16.5",
|
||||||
|
"@testing-library/react": "13.4.0",
|
||||||
"@types/common-tags": "1.8.1",
|
"@types/common-tags": "1.8.1",
|
||||||
"@types/create-react-class": "15.6.3",
|
"@types/create-react-class": "15.6.3",
|
||||||
"@types/fs-extra": "9.0.13",
|
"@types/fs-extra": "9.0.13",
|
||||||
@ -230,6 +233,7 @@
|
|||||||
"gitlab": "14.2.2",
|
"gitlab": "14.2.2",
|
||||||
"http-server": "14.1.1",
|
"http-server": "14.1.1",
|
||||||
"jest": "29.3.1",
|
"jest": "29.3.1",
|
||||||
|
"jest-environment-jsdom": "29.3.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"mockserver-client": "5.14.0",
|
"mockserver-client": "5.14.0",
|
||||||
"mockserver-node": "5.14.0",
|
"mockserver-node": "5.14.0",
|
||||||
|
1
core/src/__mocks__/@ltd/j-toml.ts
Normal file
1
core/src/__mocks__/@ltd/j-toml.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default {};
|
4
core/src/__mocks__/@staticcms/core/store/hooks.ts
Normal file
4
core/src/__mocks__/@staticcms/core/store/hooks.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
const mockDisplatch = jest.fn();
|
||||||
|
export const useAppDispatch = jest.fn().mockReturnValue(mockDisplatch);
|
||||||
|
export const useAppSelector = jest.fn();
|
2
core/src/__mocks__/@udecode/plate-core.ts
Normal file
2
core/src/__mocks__/@udecode/plate-core.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const someNode = jest.fn();
|
||||||
|
export const toggleNodeType = jest.fn();
|
@ -40,4 +40,28 @@ export {
|
|||||||
ELEMENT_UL,
|
ELEMENT_UL,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createPlateEditor = jest.fn();
|
||||||
|
export const createPluginFactory = jest.fn();
|
||||||
|
export const createPlugins = jest.fn();
|
||||||
|
export const createTEditor = jest.fn();
|
||||||
|
export const getTEditor = jest.fn();
|
||||||
|
export const useEditorRef = jest.fn();
|
||||||
|
export const useEditorState = jest.fn();
|
||||||
|
export const usePlateActions = jest.fn();
|
||||||
|
export const usePlateEditorRef = jest.fn();
|
||||||
|
export const usePlateEditorState = jest.fn();
|
||||||
|
export const usePlateSelectors = jest.fn();
|
||||||
|
export const usePlateStates = jest.fn();
|
||||||
|
export const findNodePath = jest.fn();
|
||||||
|
export const getNode = jest.fn();
|
||||||
|
export const getParentNode = jest.fn();
|
||||||
|
export const getSelectionBoundingClientRect = jest.fn();
|
||||||
|
export const getSelectionText = jest.fn();
|
||||||
|
export const isElement = jest.fn();
|
||||||
|
export const isElementEmpty = jest.fn();
|
||||||
|
export const isSelectionExpanded = jest.fn();
|
||||||
|
export const isText = jest.fn();
|
||||||
|
export const someNode = jest.fn();
|
||||||
|
export const usePlateSelection = jest.fn();
|
||||||
|
|
||||||
export default {};
|
export default {};
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export default jest.fn();
|
|
2
core/src/__mocks__/slate-react.ts
Normal file
2
core/src/__mocks__/slate-react.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
export const useFocused = jest.fn();
|
3
core/src/__mocks__/url-join.ts
Normal file
3
core/src/__mocks__/url-join.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function cleanStack(parts: string[]) {
|
||||||
|
return parts.join('/');
|
||||||
|
}
|
4
core/src/__mocks__/yaml.ts
Normal file
4
core/src/__mocks__/yaml.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const isNode = jest.fn();
|
||||||
|
export const isMap = jest.fn();
|
||||||
|
|
||||||
|
export default {};
|
@ -1,5 +1,7 @@
|
|||||||
import yamlFormatter from '../YamlFormatter';
|
import yamlFormatter from '../YamlFormatter';
|
||||||
|
|
||||||
|
jest.unmock('yaml');
|
||||||
|
|
||||||
const json = {
|
const json = {
|
||||||
keyF: 'valueF',
|
keyF: 'valueF',
|
||||||
keyA: 'valueA',
|
keyA: 'valueA',
|
||||||
|
@ -16,17 +16,18 @@ import {
|
|||||||
someNode,
|
someNode,
|
||||||
usePlateSelection,
|
usePlateSelection,
|
||||||
} from '@udecode/plate';
|
} from '@udecode/plate';
|
||||||
import { useFocused } from 'slate-react';
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useFocused } from 'slate-react';
|
||||||
|
|
||||||
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
import { useMdPlateEditorState, VOID_ELEMENTS } from '@staticcms/markdown';
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
|
import { VOID_ELEMENTS } from '../../serialization/slate/ast-types';
|
||||||
import BasicElementToolbarButtons from '../buttons/BasicElementToolbarButtons';
|
import BasicElementToolbarButtons from '../buttons/BasicElementToolbarButtons';
|
||||||
import BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
|
import BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
|
||||||
import MediaToolbarButtons from '../buttons/MediaToolbarButtons';
|
import MediaToolbarButtons from '../buttons/MediaToolbarButtons';
|
||||||
import TableToolbarButtons from '../buttons/TableToolbarButtons';
|
|
||||||
import ShortcodeToolbarButton from '../buttons/ShortcodeToolbarButton';
|
import ShortcodeToolbarButton from '../buttons/ShortcodeToolbarButton';
|
||||||
|
import TableToolbarButtons from '../buttons/TableToolbarButtons';
|
||||||
|
|
||||||
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
||||||
import type { ClientRectObject } from '@udecode/plate';
|
import type { ClientRectObject } from '@udecode/plate';
|
||||||
@ -76,6 +77,13 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
const [hasFocus, setHasFocus] = useState(false);
|
const [hasFocus, setHasFocus] = useState(false);
|
||||||
const debouncedHasFocus = useDebounce(hasFocus, 150);
|
const debouncedHasFocus = useDebounce(hasFocus, 150);
|
||||||
|
|
||||||
|
const [childFocusState, setChildFocusState] = useState<Record<string, boolean>>({});
|
||||||
|
const childHasFocus = useMemo(
|
||||||
|
() => Object.keys(childFocusState).reduce((acc, value) => acc || childFocusState[value], false),
|
||||||
|
[childFocusState],
|
||||||
|
);
|
||||||
|
const debouncedChildHasFocus = useDebounce(hasFocus, 150);
|
||||||
|
|
||||||
const handleFocus = useCallback(() => {
|
const handleFocus = useCallback(() => {
|
||||||
setHasFocus(true);
|
setHasFocus(true);
|
||||||
}, []);
|
}, []);
|
||||||
@ -84,6 +92,26 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
setHasFocus(false);
|
setHasFocus(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleChildFocus = useCallback(
|
||||||
|
(key: string) => () => {
|
||||||
|
setChildFocusState(oldState => ({
|
||||||
|
...oldState,
|
||||||
|
[key]: true,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChildBlur = useCallback(
|
||||||
|
(key: string) => () => {
|
||||||
|
setChildFocusState(oldState => ({
|
||||||
|
...oldState,
|
||||||
|
[key]: false,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const anchorEl = useRef<HTMLDivElement>();
|
const anchorEl = useRef<HTMLDivElement>();
|
||||||
const [selectionBoundingClientRect, setSelectionBoundingClientRect] =
|
const [selectionBoundingClientRect, setSelectionBoundingClientRect] =
|
||||||
useState<ClientRectObject | null>(null);
|
useState<ClientRectObject | null>(null);
|
||||||
@ -102,8 +130,8 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
const node = getNode(editor, editor.selection?.anchor.path ?? []);
|
const node = getNode(editor, editor.selection?.anchor.path ?? []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) {
|
if (!editor || !hasEditorFocus) {
|
||||||
return undefined;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -121,7 +149,14 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
const debouncedEditorFocus = useDebounce(hasEditorFocus, 150);
|
const debouncedEditorFocus = useDebounce(hasEditorFocus, 150);
|
||||||
|
|
||||||
const groups: ReactNode[] = useMemo(() => {
|
const groups: ReactNode[] = useMemo(() => {
|
||||||
if (!mediaOpen && !debouncedEditorFocus && !hasFocus && !debouncedHasFocus) {
|
if (
|
||||||
|
!mediaOpen &&
|
||||||
|
!debouncedEditorFocus &&
|
||||||
|
!hasFocus &&
|
||||||
|
!debouncedHasFocus &&
|
||||||
|
!debouncedChildHasFocus &&
|
||||||
|
!childHasFocus
|
||||||
|
) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +181,9 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
field={field}
|
field={field}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
onMediaToggle={setMediaOpen}
|
onMediaToggle={setMediaOpen}
|
||||||
hideUploads
|
hideImages
|
||||||
|
handleChildFocus={handleChildFocus}
|
||||||
|
handleChildBlur={handleChildBlur}
|
||||||
/>,
|
/>,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
}
|
}
|
||||||
@ -183,9 +220,11 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
field={field}
|
field={field}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
onMediaToggle={setMediaOpen}
|
onMediaToggle={setMediaOpen}
|
||||||
|
handleChildFocus={handleChildFocus}
|
||||||
|
handleChildBlur={handleChildBlur}
|
||||||
/>,
|
/>,
|
||||||
!useMdx ? <ShortcodeToolbarButton key="shortcode-button" /> : null,
|
!useMdx ? <ShortcodeToolbarButton key="shortcode-button" /> : null,
|
||||||
];
|
].filter(Boolean);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,7 +279,7 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Popper
|
<Popper
|
||||||
open={debouncedOpen}
|
open={Boolean(debouncedOpen && anchorEl.current)}
|
||||||
placement="top"
|
placement="top"
|
||||||
anchorEl={anchorEl.current ?? null}
|
anchorEl={anchorEl.current ?? null}
|
||||||
sx={{ zIndex: 100 }}
|
sx={{ zIndex: 100 }}
|
||||||
|
@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import {
|
||||||
|
findNodePath,
|
||||||
|
getNode,
|
||||||
|
getParentNode,
|
||||||
|
isElement,
|
||||||
|
isElementEmpty,
|
||||||
|
someNode,
|
||||||
|
usePlateEditorState,
|
||||||
|
} from '@udecode/plate';
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { useFocused } from 'slate-react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
|
import { mockMarkdownCollection, mockMarkdownField } from '@staticcms/test/data/collection';
|
||||||
|
import BalloonToolbar from '../BalloonToolbar';
|
||||||
|
|
||||||
|
import type { Entry } from '@staticcms/core/interface';
|
||||||
|
import type { MdEditor } from '@staticcms/markdown/plate/plateTypes';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
let entry: Entry;
|
||||||
|
|
||||||
|
interface BalloonToolbarWrapperProps {
|
||||||
|
useMdx?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BalloonToolbarWrapper: FC<BalloonToolbarWrapperProps> = ({ useMdx = false }) => {
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<BalloonToolbar
|
||||||
|
key="balloon-toolbar"
|
||||||
|
useMdx={useMdx}
|
||||||
|
containerRef={ref.current}
|
||||||
|
collection={mockMarkdownCollection}
|
||||||
|
field={mockMarkdownField}
|
||||||
|
entry={entry}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe(BalloonToolbar.name, () => {
|
||||||
|
const mockUseEditor = usePlateEditorState as jest.Mock;
|
||||||
|
let mockEditor: MdEditor;
|
||||||
|
|
||||||
|
const mockGetNode = getNode as jest.Mock;
|
||||||
|
const mockIsElement = isElement as unknown as jest.Mock;
|
||||||
|
const mockIsElementEmpty = isElementEmpty as jest.Mock;
|
||||||
|
const mockSomeNode = someNode as jest.Mock;
|
||||||
|
const mockUseFocused = useFocused as jest.Mock;
|
||||||
|
const mockFindNodePath = findNodePath as jest.Mock;
|
||||||
|
const mockGetParentNode = getParentNode as jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
entry = {
|
||||||
|
collection: 'posts',
|
||||||
|
slug: '2022-12-13-post-number-1',
|
||||||
|
path: '_posts/2022-12-13-post-number-1.md',
|
||||||
|
partial: false,
|
||||||
|
raw: '--- title: "This is post # 1" draft: false date: 2022-12-13T00:00:00.000Z --- # The post is number 1\n\nAnd some text',
|
||||||
|
label: '',
|
||||||
|
author: '',
|
||||||
|
mediaFiles: [],
|
||||||
|
isModification: null,
|
||||||
|
newRecord: false,
|
||||||
|
updatedOn: '',
|
||||||
|
data: {
|
||||||
|
title: 'This is post # 1',
|
||||||
|
draft: false,
|
||||||
|
date: '2022-12-13T00:00:00.000Z',
|
||||||
|
body: '# The post is number 1\n\nAnd some text',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockEditor = {
|
||||||
|
selection: undefined,
|
||||||
|
} as unknown as MdEditor;
|
||||||
|
|
||||||
|
mockUseEditor.mockReturnValue(mockEditor);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders empty div by default', () => {
|
||||||
|
render(<BalloonToolbarWrapper />);
|
||||||
|
expect(screen.queryAllByRole('button').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('empty node toolbar', () => {
|
||||||
|
interface EmptyNodeToolbarSetupOptions {
|
||||||
|
useMdx?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyNodeToolbarSetup = ({ useMdx }: EmptyNodeToolbarSetupOptions = {}) => {
|
||||||
|
mockEditor = {
|
||||||
|
selection: undefined,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'p',
|
||||||
|
children: [{ text: '' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'p',
|
||||||
|
children: [{ text: '' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as MdEditor;
|
||||||
|
|
||||||
|
mockUseEditor.mockReturnValue(mockEditor);
|
||||||
|
|
||||||
|
mockGetNode.mockReturnValue({ text: '' });
|
||||||
|
mockIsElement.mockReturnValue(true);
|
||||||
|
mockIsElementEmpty.mockReturnValue(true);
|
||||||
|
mockSomeNode.mockReturnValue(false);
|
||||||
|
mockUseFocused.mockReturnValue(true);
|
||||||
|
|
||||||
|
mockFindNodePath.mockReturnValue([1, 0]);
|
||||||
|
mockGetParentNode.mockReturnValue([
|
||||||
|
{
|
||||||
|
type: 'p',
|
||||||
|
children: [{ text: '' }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { rerender } = render(<BalloonToolbarWrapper />);
|
||||||
|
|
||||||
|
rerender(<BalloonToolbarWrapper useMdx={useMdx} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders empty node toolbar for markdown', () => {
|
||||||
|
emptyNodeToolbarSetup();
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('toolbar-button-bold')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-italic')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-code')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-strikethrough')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('toolbar-button-blockquote')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('font-type-select')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-add-table')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-insert-link')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-insert-image')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// MDX Only do not show for markdown version
|
||||||
|
expect(screen.queryByTestId('toolbar-button-underline')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders empty node toolbar for mdx', () => {
|
||||||
|
emptyNodeToolbarSetup({ useMdx: true });
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('toolbar-button-bold')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-italic')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-code')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-strikethrough')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('toolbar-button-blockquote')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('font-type-select')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-add-table')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-insert-link')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-insert-image')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('toolbar-button-underline')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -16,7 +16,7 @@ import { someNode, toggleNodeType } from '@udecode/plate-core';
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
|
|
||||||
import type { SelectChangeEvent } from '@mui/material/Select';
|
import type { SelectChangeEvent } from '@mui/material/Select';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
@ -107,6 +107,7 @@ const FontTypeSelect: FC<FontTypeSelectProps> = ({ disabled = false }) => {
|
|||||||
<StyledSelect
|
<StyledSelect
|
||||||
labelId="font-type-select-label"
|
labelId="font-type-select-label"
|
||||||
id="font-type-select"
|
id="font-type-select"
|
||||||
|
data-testid="font-type-select"
|
||||||
value={value?.type ?? ELEMENT_PARAGRAPH}
|
value={value?.type ?? ELEMENT_PARAGRAPH}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -10,12 +10,14 @@ import type { FC } from 'react';
|
|||||||
|
|
||||||
export interface MediaToolbarButtonsProps {
|
export interface MediaToolbarButtonsProps {
|
||||||
containerRef: HTMLElement | null;
|
containerRef: HTMLElement | null;
|
||||||
hideUploads?: boolean;
|
hideImages?: boolean;
|
||||||
collection: Collection<MarkdownField>;
|
collection: Collection<MarkdownField>;
|
||||||
field: MarkdownField;
|
field: MarkdownField;
|
||||||
entry: Entry;
|
entry: Entry;
|
||||||
inserting?: boolean;
|
inserting?: boolean;
|
||||||
onMediaToggle?: (open: boolean) => void;
|
onMediaToggle?: (open: boolean) => void;
|
||||||
|
handleChildFocus?: (key: string) => () => void;
|
||||||
|
handleChildBlur?: (key: string) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
|
const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
|
||||||
@ -23,8 +25,10 @@ const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
|
|||||||
collection,
|
collection,
|
||||||
field,
|
field,
|
||||||
entry,
|
entry,
|
||||||
hideUploads = false,
|
hideImages = false,
|
||||||
onMediaToggle,
|
onMediaToggle,
|
||||||
|
handleChildFocus,
|
||||||
|
handleChildBlur,
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [linkMediaOpen, setLinkMediaOpen] = useState(false);
|
const [linkMediaOpen, setLinkMediaOpen] = useState(false);
|
||||||
@ -56,8 +60,10 @@ const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
|
|||||||
entry={entry}
|
entry={entry}
|
||||||
mediaOpen={linkMediaOpen}
|
mediaOpen={linkMediaOpen}
|
||||||
onMediaToggle={setLinkMediaOpen}
|
onMediaToggle={setLinkMediaOpen}
|
||||||
|
onFocus={handleChildFocus?.('link')}
|
||||||
|
onBlur={handleChildBlur?.('link')}
|
||||||
/>
|
/>
|
||||||
{!hideUploads ? (
|
{!hideImages ? (
|
||||||
<ImageToolbarButton
|
<ImageToolbarButton
|
||||||
containerRef={containerRef}
|
containerRef={containerRef}
|
||||||
tooltip="Insert Image"
|
tooltip="Insert Image"
|
||||||
@ -68,6 +74,8 @@ const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
|
|||||||
entry={entry}
|
entry={entry}
|
||||||
mediaOpen={imageMediaOpen}
|
mediaOpen={imageMediaOpen}
|
||||||
onMediaToggle={setImageMediaOpen}
|
onMediaToggle={setImageMediaOpen}
|
||||||
|
onFocus={handleChildFocus?.('image')}
|
||||||
|
onBlur={handleChildBlur?.('image')}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
@ -6,7 +6,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { getShortcodes } from '../../../../../lib/registry';
|
import { getShortcodes } from '../../../../../lib/registry';
|
||||||
import { toTitleCase } from '../../../../../lib/util/string.util';
|
import { toTitleCase } from '../../../../../lib/util/string.util';
|
||||||
import { ELEMENT_SHORTCODE, useMdPlateEditorState } from '../../plateTypes';
|
import { ELEMENT_SHORTCODE, useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
import ToolbarButton from './common/ToolbarButton';
|
import ToolbarButton from './common/ToolbarButton';
|
||||||
|
|
||||||
import type { FC, MouseEvent } from 'react';
|
import type { FC, MouseEvent } from 'react';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { isCollapsed, KEY_ALIGN, setAlign, someNode } from '@udecode/plate';
|
import { isCollapsed, KEY_ALIGN, setAlign, someNode } from '@udecode/plate';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
|
|
||||||
import type { MdEditor } from '@staticcms/markdown';
|
import type { MdEditor } from '@staticcms/markdown';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { someNode, toggleNodeType } from '@udecode/plate';
|
import { someNode, toggleNodeType } from '@udecode/plate';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
|
|
||||||
import type { MdEditor } from '@staticcms/markdown';
|
import type { MdEditor } from '@staticcms/markdown';
|
||||||
|
@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
import { Transforms } from 'slate';
|
import { Transforms } from 'slate';
|
||||||
import { ReactEditor } from 'slate-react';
|
import { ReactEditor } from 'slate-react';
|
||||||
|
|
||||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
import ColorPicker from '../../color-picker/ColorPicker';
|
import ColorPicker from '../../color-picker/ColorPicker';
|
||||||
import ToolbarDropdown from './dropdown/ToolbarDropdown';
|
import ToolbarDropdown from './dropdown/ToolbarDropdown';
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { insertImage } from '@udecode/plate';
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
import MediaToolbarButton from './MediaToolbarButton';
|
import MediaToolbarButton from './MediaToolbarButton';
|
||||||
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
@ -2,7 +2,7 @@ import { ELEMENT_LINK, insertLink, someNode } from '@udecode/plate';
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
import MediaToolbarButton from './MediaToolbarButton';
|
import MediaToolbarButton from './MediaToolbarButton';
|
||||||
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
@ -12,10 +12,10 @@ const LinkToolbarButton: FC<Omit<MediaToolbarButtonProps, 'onChange'>> = props =
|
|||||||
const editor = useMdPlateEditorState();
|
const editor = useMdPlateEditorState();
|
||||||
const handleInsert = useCallback(
|
const handleInsert = useCallback(
|
||||||
(newUrl: string, newText: string | undefined) => {
|
(newUrl: string, newText: string | undefined) => {
|
||||||
if (isNotEmpty(newUrl) && isNotEmpty(newText)) {
|
if (isNotEmpty(newUrl)) {
|
||||||
insertLink(
|
insertLink(
|
||||||
editor,
|
editor,
|
||||||
{ url: newUrl, text: newText },
|
{ url: newUrl, text: isNotEmpty(newText) ? newText : newUrl },
|
||||||
{ at: editor.selection ?? editor.prevSelection! },
|
{ at: editor.selection ?? editor.prevSelection! },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { getListItemEntry, toggleList } from '@udecode/plate-list';
|
import { getListItemEntry, toggleList } from '@udecode/plate-list';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
|
|
||||||
import type { MdEditor } from '@staticcms/markdown';
|
import type { MdEditor } from '@staticcms/markdown';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { isMarkActive, toggleMark } from '@udecode/plate';
|
import { isMarkActive, toggleMark } from '@udecode/plate';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
|
|
||||||
import type { MdEditor } from '@staticcms/markdown';
|
import type { MdEditor } from '@staticcms/markdown';
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { focusEditor } from '@udecode/plate-core';
|
import { focusEditor } from '@udecode/plate-core';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useFocused } from 'slate-react';
|
|
||||||
|
|
||||||
import { MediaPopover, useMdPlateEditorState } from '@staticcms/markdown';
|
import MediaPopover from '../../common/MediaPopover';
|
||||||
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
|
||||||
|
|
||||||
import type { MarkdownField } from '@staticcms/core/interface';
|
import type { MarkdownField } from '@staticcms/core/interface';
|
||||||
import type { MdEditor, MediaPopoverProps } from '@staticcms/markdown';
|
import type { MdEditor, MediaPopoverProps } from '@staticcms/markdown';
|
||||||
@ -21,6 +20,8 @@ export interface MediaToolbarButtonProps
|
|||||||
mediaOpen: boolean;
|
mediaOpen: boolean;
|
||||||
onMediaToggle: (open: boolean) => void;
|
onMediaToggle: (open: boolean) => void;
|
||||||
onChange: (newUrl: string, newText: string | undefined) => void;
|
onChange: (newUrl: string, newText: string | undefined) => void;
|
||||||
|
onFocus?: () => void;
|
||||||
|
onBlur?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
|
const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
|
||||||
@ -34,23 +35,18 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
|
|||||||
mediaOpen,
|
mediaOpen,
|
||||||
onMediaToggle,
|
onMediaToggle,
|
||||||
onChange,
|
onChange,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const editor = useMdPlateEditorState();
|
const editor = useMdPlateEditorState();
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
if (forImage) {
|
|
||||||
console.log('anchorEl', anchorEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [internalUrl, setInternalUrl] = useState('');
|
const [internalUrl, setInternalUrl] = useState('');
|
||||||
const [internalText, setInternalText] = useState('');
|
const [internalText, setInternalText] = useState('');
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = useCallback(
|
||||||
(newValue: string | undefined, shouldFocus: boolean) => {
|
(newValue: string | undefined, shouldFocus: boolean) => {
|
||||||
if (forImage) {
|
|
||||||
console.log('handleClose', newValue, shouldFocus);
|
|
||||||
}
|
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
setInternalUrl('');
|
setInternalUrl('');
|
||||||
setInternalText('');
|
setInternalText('');
|
||||||
@ -72,9 +68,6 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forImage) {
|
|
||||||
console.log('handleOnClick');
|
|
||||||
}
|
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
},
|
},
|
||||||
[anchorEl, handleClose],
|
[anchorEl, handleClose],
|
||||||
@ -94,13 +87,12 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
|
|||||||
[handleClose],
|
[handleClose],
|
||||||
);
|
);
|
||||||
|
|
||||||
const editorHasFocus = useFocused();
|
|
||||||
const debouncedEditorHasFocus = useDebounce(editorHasFocus, 150);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editorHasFocus && !debouncedEditorHasFocus) {
|
if (anchorEl && !mediaOpen) {
|
||||||
handleClose(undefined, false);
|
handleClose(undefined, false);
|
||||||
}
|
}
|
||||||
}, [debouncedEditorHasFocus, editorHasFocus, handleClose]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [mediaOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -122,6 +114,8 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
|
|||||||
onMediaToggle={onMediaToggle}
|
onMediaToggle={onMediaToggle}
|
||||||
onMediaChange={handleMediaChange}
|
onMediaChange={handleMediaChange}
|
||||||
onClose={handlePopoverClose}
|
onClose={handlePopoverClose}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ import Tooltip from '@mui/material/Tooltip';
|
|||||||
import { focusEditor } from '@udecode/plate';
|
import { focusEditor } from '@udecode/plate';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
|
|
||||||
import type { MdEditor } from '@staticcms/markdown';
|
import type { MdEditor } from '@staticcms/markdown';
|
||||||
import type { FC, MouseEvent, ReactNode } from 'react';
|
import type { FC, MouseEvent, ReactNode } from 'react';
|
||||||
@ -56,6 +56,7 @@ const ToolbarButton: FC<ToolbarButtonProps> = ({
|
|||||||
aria-label={label ?? tooltip}
|
aria-label={label ?? tooltip}
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
data-testid={`toolbar-button-${label ?? tooltip}`.replace(' ', '-').toLowerCase()}
|
||||||
sx={{
|
sx={{
|
||||||
padding: '2px',
|
padding: '2px',
|
||||||
minWidth: 'unset',
|
minWidth: 'unset',
|
||||||
|
@ -20,7 +20,7 @@ const StyledPopperContent = styled('div')(
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
background: ${theme.palette.background.paper};
|
background: ${theme.palette.background.paper};
|
||||||
box-shadow: ${theme.shadows[8]};
|
box-shadow: ${theme.shadows[8]};
|
||||||
margin-bottom: 10px;
|
margin: 10px 0;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -69,6 +69,8 @@ export interface MediaPopoverProps<T extends FileOrImageField | MarkdownField> {
|
|||||||
onMediaToggle?: (open: boolean) => void;
|
onMediaToggle?: (open: boolean) => void;
|
||||||
onMediaChange: (newValue: string) => void;
|
onMediaChange: (newValue: string) => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
|
onFocus?: () => void;
|
||||||
|
onBlur?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
||||||
@ -89,6 +91,8 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
|||||||
onMediaToggle,
|
onMediaToggle,
|
||||||
onMediaChange,
|
onMediaChange,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
}: MediaPopoverProps<T>) => {
|
}: MediaPopoverProps<T>) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
@ -98,7 +102,6 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
|||||||
const [editing, setEditing] = useState(inserting);
|
const [editing, setEditing] = useState(inserting);
|
||||||
|
|
||||||
const hasEditorFocus = useFocused();
|
const hasEditorFocus = useFocused();
|
||||||
const debouncedHasEditorFocus = useDebounce(hasEditorFocus, 150);
|
|
||||||
const [hasFocus, setHasFocus] = useState(false);
|
const [hasFocus, setHasFocus] = useState(false);
|
||||||
const debouncedHasFocus = useDebounce(hasFocus, 150);
|
const debouncedHasFocus = useDebounce(hasFocus, 150);
|
||||||
|
|
||||||
@ -152,34 +155,62 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
|||||||
}
|
}
|
||||||
}, [anchorEl, editing, inserting, urlDisabled]);
|
}, [anchorEl, editing, inserting, urlDisabled]);
|
||||||
|
|
||||||
|
const [
|
||||||
|
{ prevAnchorEl, prevHasEditorFocus, prevHasFocus, prevDebouncedHasFocus },
|
||||||
|
setPrevFocusState,
|
||||||
|
] = useState<{
|
||||||
|
prevAnchorEl: HTMLElement | null;
|
||||||
|
prevHasEditorFocus: boolean;
|
||||||
|
prevHasFocus: boolean;
|
||||||
|
prevDebouncedHasFocus: boolean;
|
||||||
|
}>({
|
||||||
|
prevAnchorEl: anchorEl,
|
||||||
|
prevHasEditorFocus: hasEditorFocus,
|
||||||
|
prevHasFocus: hasFocus,
|
||||||
|
prevDebouncedHasFocus: debouncedHasFocus,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (mediaOpen) {
|
||||||
anchorEl &&
|
return;
|
||||||
!debouncedHasEditorFocus &&
|
}
|
||||||
!hasEditorFocus &&
|
|
||||||
!hasFocus &&
|
if (anchorEl && !prevHasEditorFocus && hasEditorFocus) {
|
||||||
!debouncedHasFocus &&
|
|
||||||
!mediaOpen
|
|
||||||
) {
|
|
||||||
handleClose(false);
|
handleClose(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (anchorEl && (prevHasFocus || prevDebouncedHasFocus) && !hasFocus && !debouncedHasFocus) {
|
||||||
|
handleClose(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPrevFocusState({
|
||||||
|
prevAnchorEl: anchorEl,
|
||||||
|
prevHasEditorFocus: hasEditorFocus,
|
||||||
|
prevHasFocus: hasFocus,
|
||||||
|
prevDebouncedHasFocus: debouncedHasFocus,
|
||||||
|
});
|
||||||
}, [
|
}, [
|
||||||
anchorEl,
|
anchorEl,
|
||||||
debouncedHasEditorFocus,
|
|
||||||
debouncedHasFocus,
|
debouncedHasFocus,
|
||||||
handleClose,
|
handleClose,
|
||||||
hasEditorFocus,
|
hasEditorFocus,
|
||||||
hasFocus,
|
hasFocus,
|
||||||
mediaOpen,
|
mediaOpen,
|
||||||
|
prevAnchorEl,
|
||||||
|
prevDebouncedHasFocus,
|
||||||
|
prevHasEditorFocus,
|
||||||
|
prevHasFocus,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleFocus = useCallback(() => {
|
const handleFocus = useCallback(() => {
|
||||||
setHasFocus(true);
|
setHasFocus(true);
|
||||||
}, []);
|
onFocus?.();
|
||||||
|
}, [onFocus]);
|
||||||
|
|
||||||
const handleBlur = useCallback(() => {
|
const handleBlur = useCallback(() => {
|
||||||
setHasFocus(false);
|
setHasFocus(false);
|
||||||
}, []);
|
onBlur?.();
|
||||||
|
}, [onBlur]);
|
||||||
|
|
||||||
const handleMediaChange = useCallback(
|
const handleMediaChange = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
@ -1,5 +1,6 @@
|
|||||||
export * from './balloon-toolbar';
|
export * from './balloon-toolbar';
|
||||||
export * from './buttons';
|
export * from './buttons';
|
||||||
export * from './color-picker';
|
export * from './color-picker';
|
||||||
|
export * from './common';
|
||||||
export * from './nodes';
|
export * from './nodes';
|
||||||
export * from './toolbar';
|
export * from './toolbar';
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
export * from './blockquote';
|
export * from './blockquote';
|
||||||
export * from './code-block';
|
export * from './code-block';
|
||||||
export * from './common';
|
|
||||||
export * from './headings';
|
export * from './headings';
|
||||||
export * from './horizontal-rule';
|
export * from './horizontal-rule';
|
||||||
export * from './image';
|
export * from './image';
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@udecode/plate';
|
} from '@udecode/plate';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import MediaPopover from '../common/MediaPopover';
|
import MediaPopover from '../../common/MediaPopover';
|
||||||
|
|
||||||
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
||||||
import type { MdLinkElement, MdValue } from '@staticcms/markdown';
|
import type { MdLinkElement, MdValue } from '@staticcms/markdown';
|
||||||
@ -52,10 +52,11 @@ const withLinkElement = ({ containerRef, collection, field, entry }: WithLinkEle
|
|||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(newUrl: string, newText: string) => {
|
(newUrl: string, newText: string) => {
|
||||||
|
const path = findNodePath(editor, element);
|
||||||
path && setNodes<MdLinkElement>(editor, { url: newUrl }, { at: path });
|
path && setNodes<MdLinkElement>(editor, { url: newUrl }, { at: path });
|
||||||
upsertLink(editor, { url: newUrl, text: newText });
|
upsertLink(editor, { url: newUrl, text: newText });
|
||||||
},
|
},
|
||||||
[editor, path],
|
[editor, element],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMediaChange = useCallback(
|
const handleMediaChange = useCallback(
|
||||||
|
54
core/test/data/collection.tsx
Normal file
54
core/test/data/collection.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import type { Collection, MarkdownField } from '@staticcms/core';
|
||||||
|
|
||||||
|
export const mockMarkdownField: MarkdownField = {
|
||||||
|
label: 'Body',
|
||||||
|
name: 'body',
|
||||||
|
widget: 'markdown',
|
||||||
|
hint: 'Main content goes here.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockMarkdownCollection: Collection<MarkdownField> = {
|
||||||
|
name: 'posts',
|
||||||
|
label: 'Posts',
|
||||||
|
label_singular: 'Post',
|
||||||
|
description:
|
||||||
|
'The description is a great place for tone setting, high level information, and editing guidelines that are specific to a collection.\n',
|
||||||
|
folder: '_posts',
|
||||||
|
slug: '{{year}}-{{month}}-{{day}}-{{slug}}',
|
||||||
|
summary: '{{title}} -- {{year}}/{{month}}/{{day}}',
|
||||||
|
sortable_fields: {
|
||||||
|
fields: ['title', 'date'],
|
||||||
|
default: {
|
||||||
|
field: 'title',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Title',
|
||||||
|
name: 'title',
|
||||||
|
widget: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Draft',
|
||||||
|
name: 'draft',
|
||||||
|
widget: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Publish Date',
|
||||||
|
name: 'date',
|
||||||
|
widget: 'datetime',
|
||||||
|
date_format: 'yyyy-MM-dd',
|
||||||
|
time_format: 'HH:mm',
|
||||||
|
format: "yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cover Image',
|
||||||
|
name: 'image',
|
||||||
|
widget: 'image',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
mockMarkdownField,
|
||||||
|
],
|
||||||
|
};
|
@ -1,6 +1,3 @@
|
|||||||
global.TextEncoder = TextEncoder;
|
|
||||||
global.TextDecoder = TextDecoder;
|
|
||||||
|
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
global.window = {
|
global.window = {
|
||||||
URL: {
|
URL: {
|
||||||
@ -15,3 +12,5 @@ if (typeof window === 'undefined') {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
global.URL.createObjectURL = jest.fn();
|
||||||
|
@ -54,6 +54,6 @@
|
|||||||
"@staticcms/core": ["./src"],
|
"@staticcms/core": ["./src"],
|
||||||
"@staticcms/core/*": ["./src/*"]
|
"@staticcms/core/*": ["./src/*"]
|
||||||
},
|
},
|
||||||
"types": ["@emotion/react/types/css-prop", "@types/jest"]
|
"types": ["@emotion/react/types/css-prop", "@types/jest", "@testing-library/jest-dom"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.base.json",
|
"extends": "./tsconfig.base.json",
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*", "test/**/*"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
577
core/yarn.lock
577
core/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user