fix: media popup (#225)
This commit is contained in:
parent
c1ae2a1bcc
commit
6602f37495
@ -56,7 +56,7 @@ collections:
|
||||
required: false
|
||||
- label: Body
|
||||
name: body
|
||||
widget: markdown
|
||||
widget: mdx
|
||||
hint: Main content goes here.
|
||||
- name: faq
|
||||
label: FAQ
|
||||
|
@ -14,4 +14,5 @@ module.exports = {
|
||||
},
|
||||
setupFiles: ['./test/setupEnv.js'],
|
||||
testRegex: '\\.spec\\.tsx?$',
|
||||
snapshotSerializers: ['@emotion/jest/serializer'],
|
||||
};
|
||||
|
@ -176,8 +176,11 @@
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"@babel/preset-typescript": "7.18.6",
|
||||
"@emotion/eslint-plugin": "11.10.0",
|
||||
"@emotion/jest": "11.10.5",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.10",
|
||||
"@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/create-react-class": "15.6.3",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
@ -230,6 +233,7 @@
|
||||
"gitlab": "14.2.2",
|
||||
"http-server": "14.1.1",
|
||||
"jest": "29.3.1",
|
||||
"jest-environment-jsdom": "29.3.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"mockserver-client": "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,
|
||||
};
|
||||
|
||||
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 {};
|
||||
|
@ -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';
|
||||
|
||||
jest.unmock('yaml');
|
||||
|
||||
const json = {
|
||||
keyF: 'valueF',
|
||||
keyA: 'valueA',
|
||||
|
@ -16,17 +16,18 @@ import {
|
||||
someNode,
|
||||
usePlateSelection,
|
||||
} from '@udecode/plate';
|
||||
import { useFocused } from 'slate-react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useFocused } from 'slate-react';
|
||||
|
||||
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||
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 BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
|
||||
import MediaToolbarButtons from '../buttons/MediaToolbarButtons';
|
||||
import TableToolbarButtons from '../buttons/TableToolbarButtons';
|
||||
import ShortcodeToolbarButton from '../buttons/ShortcodeToolbarButton';
|
||||
import TableToolbarButtons from '../buttons/TableToolbarButtons';
|
||||
|
||||
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
||||
import type { ClientRectObject } from '@udecode/plate';
|
||||
@ -76,6 +77,13 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
||||
const [hasFocus, setHasFocus] = useState(false);
|
||||
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(() => {
|
||||
setHasFocus(true);
|
||||
}, []);
|
||||
@ -84,6 +92,26 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
||||
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 [selectionBoundingClientRect, setSelectionBoundingClientRect] =
|
||||
useState<ClientRectObject | null>(null);
|
||||
@ -102,8 +130,8 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
||||
const node = getNode(editor, editor.selection?.anchor.path ?? []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return undefined;
|
||||
if (!editor || !hasEditorFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
@ -121,7 +149,14 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
||||
const debouncedEditorFocus = useDebounce(hasEditorFocus, 150);
|
||||
|
||||
const groups: ReactNode[] = useMemo(() => {
|
||||
if (!mediaOpen && !debouncedEditorFocus && !hasFocus && !debouncedHasFocus) {
|
||||
if (
|
||||
!mediaOpen &&
|
||||
!debouncedEditorFocus &&
|
||||
!hasFocus &&
|
||||
!debouncedHasFocus &&
|
||||
!debouncedChildHasFocus &&
|
||||
!childHasFocus
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -146,7 +181,9 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
||||
field={field}
|
||||
entry={entry}
|
||||
onMediaToggle={setMediaOpen}
|
||||
hideUploads
|
||||
hideImages
|
||||
handleChildFocus={handleChildFocus}
|
||||
handleChildBlur={handleChildBlur}
|
||||
/>,
|
||||
].filter(Boolean);
|
||||
}
|
||||
@ -183,9 +220,11 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
||||
field={field}
|
||||
entry={entry}
|
||||
onMediaToggle={setMediaOpen}
|
||||
handleChildFocus={handleChildFocus}
|
||||
handleChildBlur={handleChildBlur}
|
||||
/>,
|
||||
!useMdx ? <ShortcodeToolbarButton key="shortcode-button" /> : null,
|
||||
];
|
||||
].filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,7 +279,7 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
||||
}}
|
||||
/>
|
||||
<Popper
|
||||
open={debouncedOpen}
|
||||
open={Boolean(debouncedOpen && anchorEl.current)}
|
||||
placement="top"
|
||||
anchorEl={anchorEl.current ?? null}
|
||||
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 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 { FC } from 'react';
|
||||
@ -107,6 +107,7 @@ const FontTypeSelect: FC<FontTypeSelectProps> = ({ disabled = false }) => {
|
||||
<StyledSelect
|
||||
labelId="font-type-select-label"
|
||||
id="font-type-select"
|
||||
data-testid="font-type-select"
|
||||
value={value?.type ?? ELEMENT_PARAGRAPH}
|
||||
onChange={handleChange}
|
||||
size="small"
|
||||
|
@ -10,12 +10,14 @@ import type { FC } from 'react';
|
||||
|
||||
export interface MediaToolbarButtonsProps {
|
||||
containerRef: HTMLElement | null;
|
||||
hideUploads?: boolean;
|
||||
hideImages?: boolean;
|
||||
collection: Collection<MarkdownField>;
|
||||
field: MarkdownField;
|
||||
entry: Entry;
|
||||
inserting?: boolean;
|
||||
onMediaToggle?: (open: boolean) => void;
|
||||
handleChildFocus?: (key: string) => () => void;
|
||||
handleChildBlur?: (key: string) => () => void;
|
||||
}
|
||||
|
||||
const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
|
||||
@ -23,8 +25,10 @@ const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
|
||||
collection,
|
||||
field,
|
||||
entry,
|
||||
hideUploads = false,
|
||||
hideImages = false,
|
||||
onMediaToggle,
|
||||
handleChildFocus,
|
||||
handleChildBlur,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [linkMediaOpen, setLinkMediaOpen] = useState(false);
|
||||
@ -56,8 +60,10 @@ const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
|
||||
entry={entry}
|
||||
mediaOpen={linkMediaOpen}
|
||||
onMediaToggle={setLinkMediaOpen}
|
||||
onFocus={handleChildFocus?.('link')}
|
||||
onBlur={handleChildBlur?.('link')}
|
||||
/>
|
||||
{!hideUploads ? (
|
||||
{!hideImages ? (
|
||||
<ImageToolbarButton
|
||||
containerRef={containerRef}
|
||||
tooltip="Insert Image"
|
||||
@ -68,6 +74,8 @@ const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
|
||||
entry={entry}
|
||||
mediaOpen={imageMediaOpen}
|
||||
onMediaToggle={setImageMediaOpen}
|
||||
onFocus={handleChildFocus?.('image')}
|
||||
onBlur={handleChildBlur?.('image')}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
@ -6,7 +6,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { getShortcodes } from '../../../../../lib/registry';
|
||||
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 type { FC, MouseEvent } from 'react';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { isCollapsed, KEY_ALIGN, setAlign, someNode } from '@udecode/plate';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
|
||||
import type { MdEditor } from '@staticcms/markdown';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { someNode, toggleNodeType } from '@udecode/plate';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
|
||||
import type { MdEditor } from '@staticcms/markdown';
|
||||
|
@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Transforms } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||
import ColorPicker from '../../color-picker/ColorPicker';
|
||||
import ToolbarDropdown from './dropdown/ToolbarDropdown';
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { insertImage } from '@udecode/plate';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
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 type { FC } from 'react';
|
||||
|
@ -2,7 +2,7 @@ import { ELEMENT_LINK, insertLink, someNode } from '@udecode/plate';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
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 type { FC } from 'react';
|
||||
@ -12,10 +12,10 @@ const LinkToolbarButton: FC<Omit<MediaToolbarButtonProps, 'onChange'>> = props =
|
||||
const editor = useMdPlateEditorState();
|
||||
const handleInsert = useCallback(
|
||||
(newUrl: string, newText: string | undefined) => {
|
||||
if (isNotEmpty(newUrl) && isNotEmpty(newText)) {
|
||||
if (isNotEmpty(newUrl)) {
|
||||
insertLink(
|
||||
editor,
|
||||
{ url: newUrl, text: newText },
|
||||
{ url: newUrl, text: isNotEmpty(newText) ? newText : newUrl },
|
||||
{ at: editor.selection ?? editor.prevSelection! },
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { getListItemEntry, toggleList } from '@udecode/plate-list';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
|
||||
import type { MdEditor } from '@staticcms/markdown';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { isMarkActive, toggleMark } from '@udecode/plate';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown';
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
|
||||
import type { MdEditor } from '@staticcms/markdown';
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { focusEditor } from '@udecode/plate-core';
|
||||
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 useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||
|
||||
import type { MarkdownField } from '@staticcms/core/interface';
|
||||
import type { MdEditor, MediaPopoverProps } from '@staticcms/markdown';
|
||||
@ -21,6 +20,8 @@ export interface MediaToolbarButtonProps
|
||||
mediaOpen: boolean;
|
||||
onMediaToggle: (open: boolean) => void;
|
||||
onChange: (newUrl: string, newText: string | undefined) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
|
||||
@ -34,23 +35,18 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
|
||||
mediaOpen,
|
||||
onMediaToggle,
|
||||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
...props
|
||||
}) => {
|
||||
const editor = useMdPlateEditorState();
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
if (forImage) {
|
||||
console.log('anchorEl', anchorEl);
|
||||
}
|
||||
|
||||
const [internalUrl, setInternalUrl] = useState('');
|
||||
const [internalText, setInternalText] = useState('');
|
||||
|
||||
const handleClose = useCallback(
|
||||
(newValue: string | undefined, shouldFocus: boolean) => {
|
||||
if (forImage) {
|
||||
console.log('handleClose', newValue, shouldFocus);
|
||||
}
|
||||
setAnchorEl(null);
|
||||
setInternalUrl('');
|
||||
setInternalText('');
|
||||
@ -72,9 +68,6 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (forImage) {
|
||||
console.log('handleOnClick');
|
||||
}
|
||||
setAnchorEl(event.currentTarget);
|
||||
},
|
||||
[anchorEl, handleClose],
|
||||
@ -94,13 +87,12 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
|
||||
[handleClose],
|
||||
);
|
||||
|
||||
const editorHasFocus = useFocused();
|
||||
const debouncedEditorHasFocus = useDebounce(editorHasFocus, 150);
|
||||
useEffect(() => {
|
||||
if (!editorHasFocus && !debouncedEditorHasFocus) {
|
||||
if (anchorEl && !mediaOpen) {
|
||||
handleClose(undefined, false);
|
||||
}
|
||||
}, [debouncedEditorHasFocus, editorHasFocus, handleClose]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mediaOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -122,6 +114,8 @@ const MediaToolbarButton: FC<MediaToolbarButtonProps> = ({
|
||||
onMediaToggle={onMediaToggle}
|
||||
onMediaChange={handleMediaChange}
|
||||
onClose={handlePopoverClose}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -4,7 +4,7 @@ import Tooltip from '@mui/material/Tooltip';
|
||||
import { focusEditor } from '@udecode/plate';
|
||||
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 { FC, MouseEvent, ReactNode } from 'react';
|
||||
@ -56,6 +56,7 @@ const ToolbarButton: FC<ToolbarButtonProps> = ({
|
||||
aria-label={label ?? tooltip}
|
||||
size="small"
|
||||
color="inherit"
|
||||
data-testid={`toolbar-button-${label ?? tooltip}`.replace(' ', '-').toLowerCase()}
|
||||
sx={{
|
||||
padding: '2px',
|
||||
minWidth: 'unset',
|
||||
|
@ -20,7 +20,7 @@ const StyledPopperContent = styled('div')(
|
||||
gap: 4px;
|
||||
background: ${theme.palette.background.paper};
|
||||
box-shadow: ${theme.shadows[8]};
|
||||
margin-bottom: 10px;
|
||||
margin: 10px 0;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
@ -69,6 +69,8 @@ export interface MediaPopoverProps<T extends FileOrImageField | MarkdownField> {
|
||||
onMediaToggle?: (open: boolean) => void;
|
||||
onMediaChange: (newValue: string) => void;
|
||||
onRemove?: () => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
||||
@ -89,6 +91,8 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
||||
onMediaToggle,
|
||||
onMediaChange,
|
||||
onRemove,
|
||||
onFocus,
|
||||
onBlur,
|
||||
}: MediaPopoverProps<T>) => {
|
||||
const theme = useTheme();
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
@ -98,7 +102,6 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
||||
const [editing, setEditing] = useState(inserting);
|
||||
|
||||
const hasEditorFocus = useFocused();
|
||||
const debouncedHasEditorFocus = useDebounce(hasEditorFocus, 150);
|
||||
const [hasFocus, setHasFocus] = useState(false);
|
||||
const debouncedHasFocus = useDebounce(hasFocus, 150);
|
||||
|
||||
@ -152,34 +155,62 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
||||
}
|
||||
}, [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(() => {
|
||||
if (
|
||||
anchorEl &&
|
||||
!debouncedHasEditorFocus &&
|
||||
!hasEditorFocus &&
|
||||
!hasFocus &&
|
||||
!debouncedHasFocus &&
|
||||
!mediaOpen
|
||||
) {
|
||||
if (mediaOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (anchorEl && !prevHasEditorFocus && hasEditorFocus) {
|
||||
handleClose(false);
|
||||
}
|
||||
|
||||
if (anchorEl && (prevHasFocus || prevDebouncedHasFocus) && !hasFocus && !debouncedHasFocus) {
|
||||
handleClose(false);
|
||||
}
|
||||
|
||||
setPrevFocusState({
|
||||
prevAnchorEl: anchorEl,
|
||||
prevHasEditorFocus: hasEditorFocus,
|
||||
prevHasFocus: hasFocus,
|
||||
prevDebouncedHasFocus: debouncedHasFocus,
|
||||
});
|
||||
}, [
|
||||
anchorEl,
|
||||
debouncedHasEditorFocus,
|
||||
debouncedHasFocus,
|
||||
handleClose,
|
||||
hasEditorFocus,
|
||||
hasFocus,
|
||||
mediaOpen,
|
||||
prevAnchorEl,
|
||||
prevDebouncedHasFocus,
|
||||
prevHasEditorFocus,
|
||||
prevHasFocus,
|
||||
]);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
setHasFocus(true);
|
||||
}, []);
|
||||
onFocus?.();
|
||||
}, [onFocus]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setHasFocus(false);
|
||||
}, []);
|
||||
onBlur?.();
|
||||
}, [onBlur]);
|
||||
|
||||
const handleMediaChange = useCallback(
|
||||
(newValue: string) => {
|
@ -1,5 +1,6 @@
|
||||
export * from './balloon-toolbar';
|
||||
export * from './buttons';
|
||||
export * from './color-picker';
|
||||
export * from './common';
|
||||
export * from './nodes';
|
||||
export * from './toolbar';
|
||||
|
@ -1,6 +1,5 @@
|
||||
export * from './blockquote';
|
||||
export * from './code-block';
|
||||
export * from './common';
|
||||
export * from './headings';
|
||||
export * from './horizontal-rule';
|
||||
export * from './image';
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
} from '@udecode/plate';
|
||||
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 { MdLinkElement, MdValue } from '@staticcms/markdown';
|
||||
@ -52,10 +52,11 @@ const withLinkElement = ({ containerRef, collection, field, entry }: WithLinkEle
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newUrl: string, newText: string) => {
|
||||
const path = findNodePath(editor, element);
|
||||
path && setNodes<MdLinkElement>(editor, { url: newUrl }, { at: path });
|
||||
upsertLink(editor, { url: newUrl, text: newText });
|
||||
},
|
||||
[editor, path],
|
||||
[editor, element],
|
||||
);
|
||||
|
||||
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') {
|
||||
global.window = {
|
||||
URL: {
|
||||
@ -15,3 +12,5 @@ if (typeof window === 'undefined') {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
global.URL.createObjectURL = jest.fn();
|
||||
|
@ -54,6 +54,6 @@
|
||||
"@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",
|
||||
"include": ["src/**/*"],
|
||||
"include": ["src/**/*", "test/**/*"],
|
||||
"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