feat: markdown toolbar customization (#776)

This commit is contained in:
Daniel Lautzenheiser 2023-05-03 12:09:31 -04:00 committed by GitHub
parent a7ab1a7c0d
commit cd13f3d193
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1594 additions and 795 deletions

View File

@ -525,6 +525,42 @@ collections:
widget: markdown
media_library:
folder_support: true
- name: customized_buttons
label: Customized Buttons
widget: markdown
toolbar_buttons:
main:
- bold
- italic
- font
- shortcode
- label: Insert
groups:
- items: ['image', 'file-link']
- items: ['insert-table']
empty:
- bold
- italic
- font
- label: Insert
groups:
- items: ['image', 'file-link']
- items: ['blockquote', 'code-block']
selection:
- bold
- italic
- font
- file-link
table_empty:
- insert-row
- insert-column
- delete-row
- delete-column
- delete-table
table_selection:
- bold
- italic
- font
- name: number
label: Number
file: _widgets/number.json

View File

@ -0,0 +1,2 @@
export const getListItemEntry = jest.fn();
export const toggleList = jest.fn();

View File

@ -0,0 +1,20 @@
export const FONT_TOOLBAR_BUTTON = 'font';
export const SHORTCODE_TOOLBAR_BUTTON = 'shortcode';
export const BLOCKQUOTE_TOOLBAR_BUTTON = 'blockquote';
export const BOLD_TOOLBAR_BUTTON = 'bold';
export const CODE_BLOCK_TOOLBAR_BUTTON = 'code-block';
export const CODE_TOOLBAR_BUTTON = 'code';
export const DECREASE_IDENT_TOOLBAR_BUTTON = 'decrease-indent';
export const DELETE_COLUMN_TOOLBAR_BUTTON = 'delete-column';
export const DELETE_ROW_TOOLBAR_BUTTON = 'delete-row';
export const DELETE_TABLE_TOOLBAR_BUTTON = 'delete-table';
export const INCRASE_IDENT_TOOLBAR_BUTTON = 'increase-indent';
export const INSERT_COLUMN_TOOLBAR_BUTTON = 'insert-column';
export const IMAGE_TOOLBAR_BUTTON = 'image';
export const FILE_LINK_TOOLBAR_BUTTON = 'file-link';
export const INSERT_ROW_TOOLBAR_BUTTON = 'insert-row';
export const INSERT_TABLE_TOOLBAR_BUTTON = 'insert-table';
export const ITALIC_TOOLBAR_BUTTON = 'italic';
export const ORDERED_LIST_TOOLBAR_BUTTON = 'ordered-list';
export const STRIKETHROUGH_TOOLBAR_BUTTON = 'strikethrough';
export const UNORDERED_LIST_TOOLBAR_BUTTON = 'unordered-list';

View File

@ -15,6 +15,28 @@ import type {
SORT_DIRECTION_DESCENDING,
SORT_DIRECTION_NONE,
} from './constants';
import type {
BLOCKQUOTE_TOOLBAR_BUTTON,
BOLD_TOOLBAR_BUTTON,
CODE_BLOCK_TOOLBAR_BUTTON,
CODE_TOOLBAR_BUTTON,
DECREASE_IDENT_TOOLBAR_BUTTON,
DELETE_COLUMN_TOOLBAR_BUTTON,
DELETE_ROW_TOOLBAR_BUTTON,
DELETE_TABLE_TOOLBAR_BUTTON,
FILE_LINK_TOOLBAR_BUTTON,
FONT_TOOLBAR_BUTTON,
IMAGE_TOOLBAR_BUTTON,
INCRASE_IDENT_TOOLBAR_BUTTON,
INSERT_COLUMN_TOOLBAR_BUTTON,
INSERT_ROW_TOOLBAR_BUTTON,
INSERT_TABLE_TOOLBAR_BUTTON,
ITALIC_TOOLBAR_BUTTON,
ORDERED_LIST_TOOLBAR_BUTTON,
SHORTCODE_TOOLBAR_BUTTON,
STRIKETHROUGH_TOOLBAR_BUTTON,
UNORDERED_LIST_TOOLBAR_BUTTON,
} from './constants/toolbar_buttons';
import type { formatExtensions } from './formats/formats';
import type {
I18N_FIELD_DUPLICATE,
@ -642,8 +664,52 @@ export interface MapField extends BaseField {
height?: string;
}
export type MarkdownToolbarButtonType =
| LowLevelMarkdownToolbarButtonType
| typeof FONT_TOOLBAR_BUTTON
| typeof SHORTCODE_TOOLBAR_BUTTON;
export type LowLevelMarkdownToolbarButtonType =
| typeof BLOCKQUOTE_TOOLBAR_BUTTON
| typeof BOLD_TOOLBAR_BUTTON
| typeof CODE_BLOCK_TOOLBAR_BUTTON
| typeof CODE_TOOLBAR_BUTTON
| typeof DECREASE_IDENT_TOOLBAR_BUTTON
| typeof DELETE_COLUMN_TOOLBAR_BUTTON
| typeof DELETE_ROW_TOOLBAR_BUTTON
| typeof DELETE_TABLE_TOOLBAR_BUTTON
| typeof INCRASE_IDENT_TOOLBAR_BUTTON
| typeof INSERT_COLUMN_TOOLBAR_BUTTON
| typeof IMAGE_TOOLBAR_BUTTON
| typeof FILE_LINK_TOOLBAR_BUTTON
| typeof INSERT_ROW_TOOLBAR_BUTTON
| typeof INSERT_TABLE_TOOLBAR_BUTTON
| typeof ITALIC_TOOLBAR_BUTTON
| typeof ORDERED_LIST_TOOLBAR_BUTTON
| typeof STRIKETHROUGH_TOOLBAR_BUTTON
| typeof UNORDERED_LIST_TOOLBAR_BUTTON;
export type MarkdownToolbarItem =
| MarkdownToolbarButtonType
| {
label: string;
icon?: string;
groups: {
items: LowLevelMarkdownToolbarButtonType[];
}[];
};
export interface MarkdownFieldToolbarButtons {
main?: MarkdownToolbarItem[];
empty?: MarkdownToolbarItem[];
selection?: MarkdownToolbarItem[];
table_empty?: MarkdownToolbarItem[];
table_selection?: MarkdownToolbarItem[];
}
export interface MarkdownField extends MediaField {
widget: 'markdown';
toolbar_buttons?: MarkdownFieldToolbarButtons;
default?: string;
}

View File

@ -17,17 +17,73 @@ import { useFocused } from 'slate-react';
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
import { isEmpty } from '@staticcms/core/lib/util/string.util';
import { selectVisible } from '@staticcms/core/reducers/selectors/mediaLibrary';
import { useAppSelector } from '@staticcms/core/store/hooks';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import BasicElementToolbarButtons from '../buttons/BasicElementToolbarButtons';
import BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
import MediaToolbarButtons from '../buttons/MediaToolbarButtons';
import ShortcodeToolbarButton from '../buttons/ShortcodeToolbarButton';
import TableToolbarButtons from '../buttons/TableToolbarButtons';
import { getToolbarButtons } from '../../hooks/useToolbarButtons';
import {
BOLD_TOOLBAR_BUTTON,
CODE_TOOLBAR_BUTTON,
DELETE_COLUMN_TOOLBAR_BUTTON,
DELETE_ROW_TOOLBAR_BUTTON,
DELETE_TABLE_TOOLBAR_BUTTON,
FILE_LINK_TOOLBAR_BUTTON,
FONT_TOOLBAR_BUTTON,
IMAGE_TOOLBAR_BUTTON,
INSERT_COLUMN_TOOLBAR_BUTTON,
INSERT_ROW_TOOLBAR_BUTTON,
ITALIC_TOOLBAR_BUTTON,
SHORTCODE_TOOLBAR_BUTTON,
STRIKETHROUGH_TOOLBAR_BUTTON,
} from '@staticcms/core/constants/toolbar_buttons';
import type { Collection, MarkdownField } from '@staticcms/core/interface';
import type {
Collection,
MarkdownField,
MarkdownToolbarButtonType,
} from '@staticcms/core/interface';
import type { ClientRectObject } from '@udecode/plate';
import type { FC, ReactNode } from 'react';
const DEFAULT_EMPTY_BUTTONS: MarkdownToolbarButtonType[] = [];
const DEFAULT_SELECTION_BUTTONS: MarkdownToolbarButtonType[] = [
BOLD_TOOLBAR_BUTTON,
ITALIC_TOOLBAR_BUTTON,
STRIKETHROUGH_TOOLBAR_BUTTON,
CODE_TOOLBAR_BUTTON,
FONT_TOOLBAR_BUTTON,
FILE_LINK_TOOLBAR_BUTTON,
];
const DEFAULT_TABLE_EMPTY_BUTTONS: MarkdownToolbarButtonType[] = [
BOLD_TOOLBAR_BUTTON,
ITALIC_TOOLBAR_BUTTON,
STRIKETHROUGH_TOOLBAR_BUTTON,
CODE_TOOLBAR_BUTTON,
INSERT_ROW_TOOLBAR_BUTTON,
DELETE_ROW_TOOLBAR_BUTTON,
INSERT_COLUMN_TOOLBAR_BUTTON,
DELETE_COLUMN_TOOLBAR_BUTTON,
DELETE_TABLE_TOOLBAR_BUTTON,
FILE_LINK_TOOLBAR_BUTTON,
IMAGE_TOOLBAR_BUTTON,
SHORTCODE_TOOLBAR_BUTTON,
];
const DEFAULT_TABLE_SELECTION_BUTTONS: MarkdownToolbarButtonType[] = [
BOLD_TOOLBAR_BUTTON,
ITALIC_TOOLBAR_BUTTON,
STRIKETHROUGH_TOOLBAR_BUTTON,
CODE_TOOLBAR_BUTTON,
INSERT_ROW_TOOLBAR_BUTTON,
DELETE_ROW_TOOLBAR_BUTTON,
INSERT_COLUMN_TOOLBAR_BUTTON,
DELETE_COLUMN_TOOLBAR_BUTTON,
DELETE_TABLE_TOOLBAR_BUTTON,
FILE_LINK_TOOLBAR_BUTTON,
];
export interface BalloonToolbarProps {
useMdx: boolean;
containerRef: HTMLElement | null;
@ -49,6 +105,8 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
const [hasFocus, setHasFocus] = useState(false);
const debouncedHasFocus = useDebounce(hasFocus, 150);
const isMediaLibraryOpen = useAppSelector(selectVisible);
const handleFocus = useCallback(() => {
setHasFocus(true);
}, []);
@ -91,79 +149,57 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
const debouncedEditorFocus = useDebounce(hasEditorFocus, 150);
const groups: ReactNode[] = useMemo(() => {
const [groups, setGroups] = useState<ReactNode[]>([]);
useEffect(() => {
if (isMediaLibraryOpen) {
return;
}
if (!debouncedEditorFocus && !hasFocus && !debouncedHasFocus) {
return [];
setGroups([]);
return;
}
if (selection && someNode(editor, { match: { type: ELEMENT_LINK }, at: selection?.anchor })) {
return [];
setGroups([]);
return;
}
// Selected text buttons
if (selectionText && selectionExpanded) {
return [
<BasicMarkToolbarButtons
key="selection-basic-mark-buttons"
useMdx={useMdx}
disabled={disabled}
/>,
<BasicElementToolbarButtons
key="selection-basic-element-buttons"
hideFontTypeSelect={isInTableCell}
hideCodeBlock
disabled={disabled}
/>,
isInTableCell && (
<TableToolbarButtons key="selection-table-toolbar-buttons" disabled={disabled} />
setGroups(
getToolbarButtons(
isInTableCell
? field.toolbar_buttons?.table_selection ?? DEFAULT_TABLE_SELECTION_BUTTONS
: field.toolbar_buttons?.selection ?? DEFAULT_SELECTION_BUTTONS,
collection,
field,
disabled,
),
<MediaToolbarButtons
key="selection-media-buttons"
collection={collection}
field={field}
hideImages
disabled={disabled}
/>,
].filter(Boolean);
);
return;
}
const allButtons = [
<BasicMarkToolbarButtons
key="empty-basic-mark-buttons"
useMdx={useMdx}
disabled={disabled}
/>,
<BasicElementToolbarButtons
key="empty-basic-element-buttons"
hideFontTypeSelect={isInTableCell}
hideCodeBlock
disabled={disabled}
/>,
<TableToolbarButtons
key="empty-table-toolbar-buttons"
isInTable={isInTableCell}
disabled={disabled}
/>,
<MediaToolbarButtons
key="empty-media-buttons"
collection={collection}
field={field}
disabled={disabled}
/>,
!useMdx ? <ShortcodeToolbarButton key="shortcode-button" disabled={disabled} /> : null,
].filter(Boolean);
// Empty table cell
if (
isInTableCell &&
editor.children.length > 1 &&
node &&
((isElement(node) && isElementEmpty(editor, node)) || (isText(node) && isEmpty(node.text)))
) {
return allButtons;
setGroups(
getToolbarButtons(
isInTableCell
? field.toolbar_buttons?.table_empty ?? DEFAULT_TABLE_EMPTY_BUTTONS
: field.toolbar_buttons?.empty ?? DEFAULT_EMPTY_BUTTONS,
collection,
field,
disabled,
),
);
return;
}
return [];
setGroups([]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
debouncedEditorFocus,
@ -179,6 +215,7 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
containerRef,
collection,
field,
isMediaLibraryOpen,
]);
const [prevSelectionBoundingClientRect, setPrevSelectionBoundingClientRect] = useState(
@ -190,8 +227,8 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
prevSelectionBoundingClientRect !== selectionBoundingClientRect ? 0 : 150,
);
const open = useMemo(
() => groups.length > 0 || debouncedGroups.length > 0,
[debouncedGroups.length, groups.length],
() => groups.length > 0 || debouncedGroups.length > 0 || isMediaLibraryOpen,
[debouncedGroups.length, groups.length, isMediaLibraryOpen],
);
const debouncedOpen = useDebounce(
open,
@ -239,10 +276,11 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
"
>
<div
data-testid="balloon-toolbar"
className="
flex
gap-0.5
"
flex
gap-0.5
"
>
{groups.length > 0 ? groups : debouncedGroups}
</div>

View File

@ -2,14 +2,16 @@
* @jest-environment jsdom
*/
import '@testing-library/jest-dom';
import { screen } from '@testing-library/react';
import { act, screen } from '@testing-library/react';
import {
ELEMENT_LINK,
findNodePath,
getNode,
getParentNode,
getSelectionText,
isElement,
isElementEmpty,
isSelectionExpanded,
someNode,
usePlateEditorState,
usePlateSelection,
@ -67,8 +69,12 @@ describe(BalloonToolbar.name, () => {
const mockUseFocused = useFocused as jest.Mock;
const mockFindNodePath = findNodePath as jest.Mock;
const mockGetParentNode = getParentNode as jest.Mock;
const mockGetSelectionText = getSelectionText as jest.Mock;
const mockIsSelectionExpanded = isSelectionExpanded as jest.Mock;
beforeEach(() => {
jest.useFakeTimers();
store.dispatch(configLoaded(config as unknown as Config));
mockEditor = {
@ -78,107 +84,158 @@ describe(BalloonToolbar.name, () => {
mockUseEditor.mockReturnValue(mockEditor);
});
afterAll(() => {
jest.useRealTimers();
});
it('renders empty div by default', () => {
renderWithProviders(<BalloonToolbarWrapper />);
expect(screen.queryAllByRole('button').length).toBe(0);
});
describe('empty node toolbar inside table', () => {
interface EmptyNodeToolbarSetupOptions {
useMdx?: boolean;
}
interface BalloonToolbarSetupOptions {
inTable?: boolean;
selectedText?: string;
}
const emptyNodeToolbarSetup = ({ useMdx }: EmptyNodeToolbarSetupOptions = {}) => {
mockEditor = {
selection: {
anchor: {
path: [1, 0],
offset: 0,
},
focus: {
path: [1, 0],
offset: 0,
},
} as TRange,
children: [
{
type: 'p',
children: [{ text: '' }],
},
{
type: 'td',
children: [{ text: '' }],
},
],
} as unknown as MdEditor;
mockUseEditor.mockReturnValue(mockEditor);
mockUsePlateSelection.mockReturnValue(mockEditor.selection);
mockGetNode.mockReturnValue({ text: '' });
mockIsElement.mockReturnValue(true);
mockIsElementEmpty.mockReturnValue(true);
mockSomeNode.mockImplementation((_editor, { match: { type } }) => type !== ELEMENT_LINK);
mockUseFocused.mockReturnValue(true);
mockFindNodePath.mockReturnValue([1, 0]);
mockGetParentNode.mockReturnValue([
const emptyNodeToolbarSetup = ({ inTable, selectedText }: BalloonToolbarSetupOptions = {}) => {
mockEditor = {
selection: {
anchor: {
path: [1, 0],
offset: 0,
},
focus: {
path: [1, 0],
offset: 0,
},
} as TRange,
children: [
{
type: 'p',
children: [{ text: !inTable && selectedText ? selectedText : '' }],
},
{
type: 'td',
children: [{ text: '' }],
children: [{ text: inTable && selectedText ? selectedText : '' }],
},
]);
],
} as unknown as MdEditor;
const result = renderWithProviders(<BalloonToolbarWrapper />);
mockUseEditor.mockReturnValue(mockEditor);
mockUsePlateSelection.mockReturnValue(mockEditor.selection);
result.rerender(<BalloonToolbarWrapper useMdx={useMdx} />);
mockGetNode.mockReturnValue({ text: '' });
mockIsElement.mockReturnValue(true);
mockIsElementEmpty.mockReturnValue(true);
mockUseFocused.mockReturnValue(true);
return result;
};
if (selectedText) {
mockIsSelectionExpanded.mockReturnValue(true);
mockGetSelectionText.mockReturnValue(selectedText);
} else {
mockIsSelectionExpanded.mockReturnValue(false);
mockGetSelectionText.mockReturnValue('');
}
it('renders empty node toolbar for markdown', () => {
emptyNodeToolbarSetup();
if (inTable) {
mockSomeNode.mockImplementation((_editor, { match: { type } }) => type !== ELEMENT_LINK);
mockFindNodePath.mockReturnValue([1, 0]);
} else {
mockSomeNode.mockReturnValue(false);
mockFindNodePath.mockReturnValue([0, 0]);
}
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();
mockGetParentNode.mockReturnValue([
{
type: 'td',
children: [{ text: '' }],
},
]);
expect(screen.queryByTestId('font-type-select')).not.toBeInTheDocument();
const result = renderWithProviders(<BalloonToolbarWrapper />);
expect(screen.queryByTestId('toolbar-button-insert-row')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-row')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-column')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-column')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-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();
act(() => {
jest.advanceTimersByTime(1000);
});
it('renders empty node toolbar for mdx', () => {
emptyNodeToolbarSetup({ useMdx: true });
result.rerender(<BalloonToolbarWrapper />);
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();
return result;
};
expect(screen.queryByTestId('font-type-select')).not.toBeInTheDocument();
it('does not render empty node toolbar', () => {
emptyNodeToolbarSetup();
expect(screen.queryByTestId('toolbar-button-insert-row')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-row')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-column')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-column')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-table')).toBeInTheDocument();
expect(screen.queryByTestId('balloon-toolbar')).not.toBeInTheDocument();
});
expect(screen.queryByTestId('toolbar-button-insert-link')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-image')).toBeInTheDocument();
it('renders selected node toolbar when text is selected', () => {
emptyNodeToolbarSetup({ selectedText: 'Test Text' });
expect(screen.queryByTestId('toolbar-button-underline')).toBeInTheDocument();
});
expect(screen.queryByTestId('balloon-toolbar')).toBeInTheDocument();
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('font-type-select')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-row')).not.toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-row')).not.toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-column')).not.toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-column')).not.toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-table')).not.toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-link')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-image')).not.toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-shortcode')).not.toBeInTheDocument();
});
it('renders empty table node toolbar when in table', () => {
emptyNodeToolbarSetup({ inTable: true });
expect(screen.queryByTestId('balloon-toolbar')).toBeInTheDocument();
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('font-type-select')).not.toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-row')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-row')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-column')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-column')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-table')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-link')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-image')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-shortcode')).toBeInTheDocument();
});
it('renders selected table node toolbar when text is selected in table', () => {
emptyNodeToolbarSetup({ inTable: true, selectedText: 'Test Text' });
expect(screen.queryByTestId('balloon-toolbar')).toBeInTheDocument();
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('font-type-select')).not.toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-row')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-row')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-insert-column')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-column')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-delete-table')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-link')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-image')).not.toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-shortcode')).not.toBeInTheDocument();
});
});

View File

@ -1,112 +0,0 @@
import { TableAdd } from '@styled-icons/fluentui-system-regular/TableAdd';
import { Add as AddIcon } from '@styled-icons/material/Add';
import { Code as CodeIcon } from '@styled-icons/material/Code';
import { FormatQuote as FormatQuoteIcon } from '@styled-icons/material/FormatQuote';
import {
ELEMENT_BLOCKQUOTE,
ELEMENT_CODE_BLOCK,
ELEMENT_IMAGE,
ELEMENT_LINK,
ELEMENT_TABLE,
insertEmptyCodeBlock,
insertTable,
toggleNodeType,
} from '@udecode/plate';
import React, { useCallback } from 'react';
import Menu from '@staticcms/core/components/common/menu/Menu';
import MenuGroup from '@staticcms/core/components/common/menu/MenuGroup';
import MenuItemButton from '@staticcms/core/components/common/menu/MenuItemButton';
import { useMdPlateEditorState } from '../../plateTypes';
import ImageToolbarButton from './common/ImageToolbarButton';
import LinkToolbarButton from './common/LinkToolbarButton';
import type { Collection, MarkdownField } from '@staticcms/core/interface';
import type { FC } from 'react';
interface AddButtonsProps {
collection: Collection<MarkdownField>;
field: MarkdownField;
disabled: boolean;
}
const AddButtons: FC<AddButtonsProps> = ({ collection, field, disabled }) => {
const editor = useMdPlateEditorState();
const handleBlockOnClick = useCallback(
(type: string, inactiveType?: string) => () => {
toggleNodeType(editor, { activeType: type, inactiveType });
},
[editor],
);
const handleCodeBlockOnClick = useCallback(() => {
insertEmptyCodeBlock(editor, {
insertNodesOptions: { select: true },
});
}, [editor]);
const handleTableAdd = useCallback(() => {
insertTable(editor, {
rowCount: 2,
colCount: 2,
});
}, [editor]);
return (
<Menu
label={<AddIcon className="h-5 w-5" aria-hidden="true" />}
data-testid="toolbar-add-buttons"
keepMounted
hideDropdownIcon
variant="text"
className="
py-0.5
px-0.5
h-7
w-7
"
disabled={disabled}
>
<MenuGroup>
<MenuItemButton
key={ELEMENT_BLOCKQUOTE}
onClick={handleBlockOnClick(ELEMENT_BLOCKQUOTE)}
startIcon={FormatQuoteIcon}
>
Blockquote
</MenuItemButton>
<MenuItemButton
key={ELEMENT_CODE_BLOCK}
onClick={handleCodeBlockOnClick}
startIcon={CodeIcon}
>
Code Block
</MenuItemButton>
</MenuGroup>
<MenuGroup>
<MenuItemButton key={ELEMENT_TABLE} onClick={handleTableAdd} startIcon={TableAdd}>
Table
</MenuItemButton>
</MenuGroup>
<MenuGroup>
<ImageToolbarButton
key={ELEMENT_IMAGE}
collection={collection}
field={field}
variant="menu"
disabled={disabled}
/>
<LinkToolbarButton
key={ELEMENT_LINK}
collection={collection}
field={field}
variant="menu"
disabled={disabled}
/>
</MenuGroup>
</Menu>
);
};
export default AddButtons;

View File

@ -1,42 +0,0 @@
import { FormatAlignCenter as FormatAlignCenterIcon } from '@styled-icons/material/FormatAlignCenter';
import { FormatAlignLeft as FormatAlignLeftIcon } from '@styled-icons/material/FormatAlignLeft';
import { FormatAlignRight as FormatAlignRightIcon } from '@styled-icons/material/FormatAlignRight';
import React from 'react';
import AlignToolbarButton from './common/AlignToolbarButton';
import type { FC } from 'react';
interface AlignToolbarButtonsProps {
disabled: boolean;
}
const AlignToolbarButtons: FC<AlignToolbarButtonsProps> = ({ disabled }) => {
return (
<>
<AlignToolbarButton
key="algin-button-left"
tooltip="Align Left"
value="left"
icon={<FormatAlignLeftIcon className="h-5 w-5" />}
disabled={disabled}
/>
<AlignToolbarButton
key="algin-button-center"
tooltip="Align Center"
value="center"
icon={<FormatAlignCenterIcon className="h-5 w-5" />}
disabled={disabled}
/>
<AlignToolbarButton
key="algin-button-right"
tooltip="Align Right"
value="right"
icon={<FormatAlignRightIcon className="h-5 w-5" />}
disabled={disabled}
/>
</>
);
};
export default AlignToolbarButtons;

View File

@ -1,24 +0,0 @@
import React from 'react';
import FontTypeSelect from './FontTypeSelect';
import type { FC } from 'react';
export interface BasicElementToolbarButtonsProps {
hideFontTypeSelect?: boolean;
disableFontTypeSelect?: boolean;
hideCodeBlock?: boolean;
disabled: boolean;
}
const BasicElementToolbarButtons: FC<BasicElementToolbarButtonsProps> = ({
hideFontTypeSelect = false,
disableFontTypeSelect = false,
disabled,
}) => {
return !hideFontTypeSelect ? (
<FontTypeSelect disabled={disableFontTypeSelect || disabled} />
) : null;
};
export default BasicElementToolbarButtons;

View File

@ -1,93 +0,0 @@
import { Code as CodeIcon } from '@styled-icons/material/Code';
import { FormatBold as FormatBoldIcon } from '@styled-icons/material/FormatBold';
import { FormatItalic as FormatItalicIcon } from '@styled-icons/material/FormatItalic';
import { FormatStrikethrough as FormatStrikethroughIcon } from '@styled-icons/material/FormatStrikethrough';
import { FormatUnderlined as FormatUnderlinedIcon } from '@styled-icons/material/FormatUnderlined';
import { Subscript as SubscriptIcon } from '@styled-icons/material/Subscript';
import { Superscript as SuperscriptIcon } from '@styled-icons/material/Superscript';
import {
MARK_BOLD,
MARK_CODE,
MARK_ITALIC,
MARK_STRIKETHROUGH,
MARK_SUBSCRIPT,
MARK_SUPERSCRIPT,
MARK_UNDERLINE,
} from '@udecode/plate';
import React from 'react';
import MarkToolbarButton from './common/MarkToolbarButton';
import type { FC } from 'react';
export interface BasicMarkToolbarButtonsProps {
extended?: boolean;
useMdx: boolean;
disabled: boolean;
}
const BasicMarkToolbarButtons: FC<BasicMarkToolbarButtonsProps> = ({
extended = false,
useMdx,
disabled,
}) => {
return (
<>
<MarkToolbarButton
tooltip="Bold"
type={MARK_BOLD}
icon={<FormatBoldIcon className="h-5 w-5" />}
disabled={disabled}
/>
<MarkToolbarButton
tooltip="Italic"
type={MARK_ITALIC}
icon={<FormatItalicIcon className="h-5 w-5" />}
disabled={disabled}
/>
{useMdx ? (
<MarkToolbarButton
key="underline-button"
tooltip="Underline"
type={MARK_UNDERLINE}
icon={<FormatUnderlinedIcon className="h-5 w-5" />}
disabled={disabled}
/>
) : null}
<MarkToolbarButton
tooltip="Strikethrough"
type={MARK_STRIKETHROUGH}
icon={<FormatStrikethroughIcon className="h-5 w-5" />}
disabled={disabled}
/>
<MarkToolbarButton
tooltip="Code"
type={MARK_CODE}
icon={<CodeIcon className="h-5 w-5" />}
disabled={disabled}
/>
{useMdx && extended ? (
<>
<MarkToolbarButton
key="superscript-button"
tooltip="Superscript"
type={MARK_SUPERSCRIPT}
clear={MARK_SUBSCRIPT}
icon={<SuperscriptIcon className="h-5 w-5" />}
disabled={disabled}
/>
<MarkToolbarButton
key="subscript-button"
tooltip="Subscript"
type={MARK_SUBSCRIPT}
clear={MARK_SUPERSCRIPT}
icon={<SubscriptIcon className="h-5 w-5" />}
disabled={disabled}
/>
</>
) : null}
</>
);
};
export default BasicMarkToolbarButtons;

View File

@ -0,0 +1,27 @@
import { FormatQuote as FormatQuoteIcon } from '@styled-icons/material/FormatQuote';
import { ELEMENT_BLOCKQUOTE } from '@udecode/plate';
import React from 'react';
import BlockToolbarButton from './common/BlockToolbarButton';
import type { FC } from 'react';
export interface BlockquoteToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const BlockquoteToolbarButton: FC<BlockquoteToolbarButtonProps> = ({ disabled, variant }) => {
return (
<BlockToolbarButton
label="Blockquote"
tooltip="Insert blockquote"
icon={FormatQuoteIcon}
type={ELEMENT_BLOCKQUOTE}
disabled={disabled}
variant={variant}
/>
);
};
export default BlockquoteToolbarButton;

View File

@ -0,0 +1,26 @@
import { FormatBold as FormatBoldIcon } from '@styled-icons/material/FormatBold';
import { MARK_BOLD } from '@udecode/plate';
import React from 'react';
import MarkToolbarButton from './common/MarkToolbarButton';
import type { FC } from 'react';
export interface BoldToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const BoldToolbarButton: FC<BoldToolbarButtonProps> = ({ disabled, variant }) => {
return (
<MarkToolbarButton
tooltip="Bold"
type={MARK_BOLD}
variant={variant}
icon={FormatBoldIcon}
disabled={disabled}
/>
);
};
export default BoldToolbarButton;

View File

@ -0,0 +1,36 @@
import { Code as CodeIcon } from '@styled-icons/material/Code';
import { insertEmptyCodeBlock } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { FC } from 'react';
export interface CodeBlockToolbarButtonsProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const CodeBlockToolbarButtons: FC<CodeBlockToolbarButtonsProps> = ({ disabled, variant }) => {
const editor = useMdPlateEditorState();
const handleCodeBlockOnClick = useCallback(() => {
insertEmptyCodeBlock(editor, {
insertNodesOptions: { select: true },
});
}, [editor]);
return (
<ToolbarButton
label="Code block"
tooltip="Insert code block"
icon={CodeIcon}
onClick={handleCodeBlockOnClick}
disabled={disabled}
variant={variant}
/>
);
};
export default CodeBlockToolbarButtons;

View File

@ -0,0 +1,26 @@
import { Code as CodeIcon } from '@styled-icons/material/Code';
import { MARK_CODE } from '@udecode/plate';
import React from 'react';
import MarkToolbarButton from './common/MarkToolbarButton';
import type { FC } from 'react';
export interface CodeToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const CodeToolbarButton: FC<CodeToolbarButtonProps> = ({ disabled, variant }) => {
return (
<MarkToolbarButton
tooltip="Code"
type={MARK_CODE}
icon={CodeIcon}
disabled={disabled}
variant={variant}
/>
);
};
export default CodeToolbarButton;

View File

@ -1,35 +0,0 @@
import { FontDownload as FontDownloadIcon } from '@styled-icons/material/FontDownload';
import { FormatColorText as FormatColorTextIcon } from '@styled-icons/material/FormatColorText';
import { MARK_BG_COLOR, MARK_COLOR } from '@udecode/plate';
import React from 'react';
import ColorPickerToolbarDropdown from './common/ColorPickerToolbarDropdown';
import type { FC } from 'react';
interface ColorToolbarButtonsProps {
disabled: boolean;
}
const ColorToolbarButtons: FC<ColorToolbarButtonsProps> = ({ disabled }) => {
return (
<>
<ColorPickerToolbarDropdown
key="color-picker-button"
pluginKey={MARK_COLOR}
icon={<FormatColorTextIcon className="h-5 w-5" />}
tooltip="Color"
disabled={disabled}
/>
<ColorPickerToolbarDropdown
key="background-color-picker-button"
pluginKey={MARK_BG_COLOR}
icon={<FontDownloadIcon className="h-5 w-5" />}
tooltip="Background Color"
disabled={disabled}
/>
</>
);
};
export default ColorToolbarButtons;

View File

@ -0,0 +1,36 @@
import { FormatIndentDecrease as FormatIndentDecreaseIcon } from '@styled-icons/material/FormatIndentDecrease';
import { outdent } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { FC } from 'react';
export interface DecreaseIndentToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const DecreaseIndentToolbarButton: FC<DecreaseIndentToolbarButtonProps> = ({
disabled,
variant,
}) => {
const editor = useMdPlateEditorState();
const handleOutdent = useCallback(() => {
outdent(editor);
}, [editor]);
return (
<ToolbarButton
tooltip="Decrease indent"
onClick={handleOutdent}
icon={FormatIndentDecreaseIcon}
disabled={disabled}
variant={variant}
/>
);
};
export default DecreaseIndentToolbarButton;

View File

@ -0,0 +1,33 @@
import { TableDeleteColumn } from '@styled-icons/fluentui-system-regular/TableDeleteColumn';
import { deleteColumn } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { FC } from 'react';
export interface DeleteColumnToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const DeleteColumnToolbarButton: FC<DeleteColumnToolbarButtonProps> = ({ disabled, variant }) => {
const editor = useMdPlateEditorState();
const handleDeleteColumn = useCallback(() => {
deleteColumn(editor);
}, [editor]);
return (
<ToolbarButton
tooltip="Delete column"
icon={TableDeleteColumn}
onClick={handleDeleteColumn}
disabled={disabled}
variant={variant}
/>
);
};
export default DeleteColumnToolbarButton;

View File

@ -0,0 +1,33 @@
import { TableDeleteRow } from '@styled-icons/fluentui-system-regular/TableDeleteRow';
import { deleteRow } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { FC } from 'react';
export interface DeleteRowToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const DeleteRowToolbarButton: FC<DeleteRowToolbarButtonProps> = ({ disabled, variant }) => {
const editor = useMdPlateEditorState();
const handleDeleteRow = useCallback(() => {
deleteRow(editor);
}, [editor]);
return (
<ToolbarButton
tooltip="Delete row"
icon={TableDeleteRow}
onClick={handleDeleteRow}
disabled={disabled}
variant={variant}
/>
);
};
export default DeleteRowToolbarButton;

View File

@ -0,0 +1,33 @@
import { TableDismiss } from '@styled-icons/fluentui-system-regular/TableDismiss';
import { deleteTable } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { FC } from 'react';
export interface DeleteTableToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const DeleteTableToolbarButton: FC<DeleteTableToolbarButtonProps> = ({ disabled, variant }) => {
const editor = useMdPlateEditorState();
const handleDeleteTable = useCallback(() => {
deleteTable(editor);
}, [editor]);
return (
<ToolbarButton
tooltip="Delete table"
icon={TableDismiss}
onClick={handleDeleteTable}
disabled={disabled}
variant={variant}
/>
);
};
export default DeleteTableToolbarButton;

View File

@ -0,0 +1,36 @@
import { FormatIndentIncrease as FormatIndentIncreaseIcon } from '@styled-icons/material/FormatIndentIncrease';
import { indent } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { FC } from 'react';
export interface IncreaseIndentToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const IncreaseIndentToolbarButton: FC<IncreaseIndentToolbarButtonProps> = ({
disabled,
variant,
}) => {
const editor = useMdPlateEditorState();
const handleIndent = useCallback(() => {
indent(editor);
}, [editor]);
return (
<ToolbarButton
tooltip="Increase indent"
onClick={handleIndent}
icon={FormatIndentIncreaseIcon}
disabled={disabled}
variant={variant}
/>
);
};
export default IncreaseIndentToolbarButton;

View File

@ -0,0 +1,33 @@
import { TableInsertColumn } from '@styled-icons/fluentui-system-regular/TableInsertColumn';
import { insertTableColumn } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { FC } from 'react';
export interface InsertColumnToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const InsertColumnToolbarButton: FC<InsertColumnToolbarButtonProps> = ({ disabled, variant }) => {
const editor = useMdPlateEditorState();
const handleInsertTableColumn = useCallback(() => {
insertTableColumn(editor);
}, [editor]);
return (
<ToolbarButton
tooltip="Insert column"
icon={TableInsertColumn}
onClick={handleInsertTableColumn}
disabled={disabled}
variant={variant}
/>
);
};
export default InsertColumnToolbarButton;

View File

@ -1,26 +1,25 @@
import { Image as ImageIcon } from '@styled-icons/material/Image';
import { ELEMENT_IMAGE, insertImage } from '@udecode/plate';
import { insertImage } from '@udecode/plate';
import React, { useCallback, useMemo } from 'react';
import MenuItemButton from '@staticcms/core/components/common/menu/MenuItemButton';
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './ToolbarButton';
import ToolbarButton from './common/ToolbarButton';
import type { Collection, MarkdownField, MediaPath } from '@staticcms/core/interface';
import type { FC } from 'react';
interface ImageToolbarButtonProps {
variant?: 'button' | 'menu';
export interface InsertImageToolbarButtonProps {
variant: 'button' | 'menu';
currentValue?: { url: string; alt?: string };
collection: Collection<MarkdownField>;
field: MarkdownField;
disabled: boolean;
}
const ImageToolbarButton: FC<ImageToolbarButtonProps> = ({
variant = 'button',
const InsertImageToolbarButton: FC<InsertImageToolbarButtonProps> = ({
variant,
field,
collection,
currentValue,
@ -47,23 +46,16 @@ const ImageToolbarButton: FC<ImageToolbarButtonProps> = ({
handleInsert,
);
if (variant === 'menu') {
return (
<MenuItemButton key={ELEMENT_IMAGE} onClick={openMediaLibrary} startIcon={ImageIcon}>
Image
</MenuItemButton>
);
}
return (
<ToolbarButton
key="insertImage"
tooltip="Insert Image"
icon={<ImageIcon className="w-5 h-5" />}
onClick={(_editor, event) => openMediaLibrary(event)}
label="Image"
tooltip="Insert image"
icon={ImageIcon}
onClick={openMediaLibrary}
disabled={disabled}
variant={variant}
/>
);
};
export default ImageToolbarButton;
export default InsertImageToolbarButton;

View File

@ -1,27 +1,26 @@
import { Link as LinkIcon } from '@styled-icons/material/Link';
import { ELEMENT_LINK, insertLink, someNode } from '@udecode/plate';
import { ELEMENT_LINK, getSelectionText, insertLink, someNode } from '@udecode/plate';
import React, { useCallback, useMemo } from 'react';
import MenuItemButton from '@staticcms/core/components/common/menu/MenuItemButton';
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './ToolbarButton';
import ToolbarButton from './common/ToolbarButton';
import type { Collection, MarkdownField, MediaPath } from '@staticcms/core/interface';
import type { FC } from 'react';
interface LinkToolbarButtonProps {
variant?: 'button' | 'menu';
export interface InsertLinkToolbarButtonProps {
variant: 'button' | 'menu';
currentValue?: { url: string; alt?: string };
collection: Collection<MarkdownField>;
field: MarkdownField;
disabled: boolean;
}
const LinkToolbarButton: FC<LinkToolbarButtonProps> = ({
variant = 'button',
const InsertLinkToolbarButton: FC<InsertLinkToolbarButtonProps> = ({
variant,
field,
collection,
currentValue,
@ -45,34 +44,35 @@ const LinkToolbarButton: FC<LinkToolbarButtonProps> = ({
const isLink = !!editor?.selection && someNode(editor, { match: { type: ELEMENT_LINK } });
const selectedText: string = useMemo(() => {
if (!editor.selection) {
return '';
}
return getSelectionText(editor);
}, [editor]);
const controlID = useUUID();
const openMediaLibrary = useMediaInsert(
{
path: currentValue?.url ?? '',
alt: currentValue?.alt,
alt: currentValue?.alt ?? selectedText,
},
{ collection, field, controlID, forImage: false, insertOptions: { chooseUrl, showAlt: true } },
handleInsert,
);
if (variant === 'menu') {
return (
<MenuItemButton key={ELEMENT_LINK} onClick={openMediaLibrary} startIcon={LinkIcon}>
File / Link
</MenuItemButton>
);
}
return (
<ToolbarButton
key="insertLink"
tooltip="Insert Link"
icon={<LinkIcon className="w-5 h-5" />}
onClick={(_editor, event) => openMediaLibrary(event)}
label="Link"
tooltip="Insert link"
icon={LinkIcon}
onClick={openMediaLibrary}
active={isLink}
disabled={disabled}
variant={variant}
/>
);
};
export default LinkToolbarButton;
export default InsertLinkToolbarButton;

View File

@ -0,0 +1,33 @@
import { TableInsertRow } from '@styled-icons/fluentui-system-regular/TableInsertRow';
import { insertTableRow } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { FC } from 'react';
export interface InsertRowToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const InsertRowToolbarButton: FC<InsertRowToolbarButtonProps> = ({ disabled, variant }) => {
const editor = useMdPlateEditorState();
const handleInsertTableRow = useCallback(() => {
insertTableRow(editor);
}, [editor]);
return (
<ToolbarButton
tooltip="Insert row"
icon={TableInsertRow}
onClick={handleInsertTableRow}
disabled={disabled}
variant={variant}
/>
);
};
export default InsertRowToolbarButton;

View File

@ -0,0 +1,40 @@
import { TableAdd } from '@styled-icons/fluentui-system-regular/TableAdd';
import { insertTable } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { FC } from 'react';
export interface InsertTableToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const InsertTableToolbarButton: FC<InsertTableToolbarButtonProps> = ({
disabled,
variant = 'button',
}) => {
const editor = useMdPlateEditorState();
const handleTableAdd = useCallback(() => {
insertTable(editor, {
rowCount: 2,
colCount: 2,
});
}, [editor]);
return (
<ToolbarButton
label="Table"
tooltip="Insert table"
icon={TableAdd}
onClick={handleTableAdd}
disabled={disabled}
variant={variant}
/>
);
};
export default InsertTableToolbarButton;

View File

@ -0,0 +1,26 @@
import { FormatItalic as FormatItalicIcon } from '@styled-icons/material/FormatItalic';
import { MARK_ITALIC } from '@udecode/plate';
import React from 'react';
import MarkToolbarButton from './common/MarkToolbarButton';
import type { FC } from 'react';
export interface ItalicToolbarButtonsProp {
disabled: boolean;
variant: 'button' | 'menu';
}
const ItalicToolbarButton: FC<ItalicToolbarButtonsProp> = ({ disabled, variant }) => {
return (
<MarkToolbarButton
tooltip="Italic"
type={MARK_ITALIC}
variant={variant}
icon={FormatItalicIcon}
disabled={disabled}
/>
);
};
export default ItalicToolbarButton;

View File

@ -1,60 +0,0 @@
import { FormatIndentDecrease as FormatIndentDecreaseIcon } from '@styled-icons/material/FormatIndentDecrease';
import { FormatIndentIncrease as FormatIndentIncreaseIcon } from '@styled-icons/material/FormatIndentIncrease';
import { FormatListBulleted as FormatListBulletedIcon } from '@styled-icons/material/FormatListBulleted';
import { FormatListNumbered as FormatListNumberedIcon } from '@styled-icons/material/FormatListNumbered';
import { ELEMENT_OL, ELEMENT_UL, getPluginType, indent, outdent } from '@udecode/plate';
import React, { useCallback } from 'react';
import { useMdPlateEditorRef } from '@staticcms/markdown';
import ListToolbarButton from './common/ListToolbarButton';
import ToolbarButton from './common/ToolbarButton';
import type { FC } from 'react';
import type { MdEditor } from '@staticcms/markdown';
interface ListToolbarButtonsProps {
disabled: boolean;
}
const ListToolbarButtons: FC<ListToolbarButtonsProps> = ({ disabled }) => {
const editor = useMdPlateEditorRef();
const handleOutdent = useCallback((editor: MdEditor) => {
outdent(editor);
}, []);
const handleIndent = useCallback((editor: MdEditor) => {
indent(editor);
}, []);
return (
<>
<ListToolbarButton
tooltip="List"
type={ELEMENT_UL}
icon={<FormatListBulletedIcon className="h-5 w-5" />}
disabled={disabled}
/>
<ListToolbarButton
tooltip="Numbered List"
type={getPluginType(editor, ELEMENT_OL)}
icon={<FormatListNumberedIcon className="h-5 w-5" />}
disabled={disabled}
/>
<ToolbarButton
tooltip="Outdent"
onClick={handleOutdent}
icon={<FormatIndentDecreaseIcon className="h-5 w-5" />}
disabled={disabled}
/>
<ToolbarButton
tooltip="Indent"
onClick={handleIndent}
icon={<FormatIndentIncreaseIcon className="h-5 w-5" />}
disabled={disabled}
/>
</>
);
};
export default ListToolbarButtons;

View File

@ -1,42 +0,0 @@
import React from 'react';
import ImageToolbarButton from './common/ImageToolbarButton';
import LinkToolbarButton from './common/LinkToolbarButton';
import type { Collection, MarkdownField } from '@staticcms/core/interface';
import type { FC } from 'react';
export interface MediaToolbarButtonsProps {
collection: Collection<MarkdownField>;
field: MarkdownField;
hideImages?: boolean;
disabled: boolean;
}
const MediaToolbarButtons: FC<MediaToolbarButtonsProps> = ({
collection,
field,
hideImages = false,
disabled,
}) => {
return (
<>
<LinkToolbarButton
key="link-button"
collection={collection}
field={field}
disabled={disabled}
/>
{!hideImages ? (
<ImageToolbarButton
key="image-button"
collection={collection}
field={field}
disabled={disabled}
/>
) : null}
</>
);
};
export default MediaToolbarButtons;

View File

@ -0,0 +1,26 @@
import { FormatListNumbered as FormatListNumberedIcon } from '@styled-icons/material/FormatListNumbered';
import { ELEMENT_OL } from '@udecode/plate';
import React from 'react';
import ListToolbarButton from './common/ListToolbarButton';
import type { FC } from 'react';
export interface OrderedListToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const OrderedListToolbarButton: FC<OrderedListToolbarButtonProps> = ({ disabled, variant }) => {
return (
<ListToolbarButton
tooltip="Numbered list"
type={ELEMENT_OL}
icon={FormatListNumberedIcon}
disabled={disabled}
variant={variant}
/>
);
};
export default OrderedListToolbarButton;

View File

@ -11,7 +11,7 @@ import { ELEMENT_SHORTCODE, useMdPlateEditorState } from '@staticcms/markdown/pl
import type { FC } from 'react';
interface ShortcodeToolbarButtonProps {
export interface ShortcodeToolbarButtonProps {
disabled: boolean;
}
@ -36,7 +36,7 @@ const ShortcodeToolbarButton: FC<ShortcodeToolbarButtonProps> = ({ disabled }) =
return (
<Menu
label={<DataArrayIcon className="h-5 w-5" aria-hidden="true" />}
data-testid="add-buttons"
data-testid="toolbar-button-shortcode"
keepMounted
hideDropdownIcon
variant="text"

View File

@ -0,0 +1,26 @@
import { FormatStrikethrough as FormatStrikethroughIcon } from '@styled-icons/material/FormatStrikethrough';
import { MARK_STRIKETHROUGH } from '@udecode/plate';
import React from 'react';
import MarkToolbarButton from './common/MarkToolbarButton';
import type { FC } from 'react';
export interface StrikethroughToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const StrikethroughToolbarButton: FC<StrikethroughToolbarButtonProps> = ({ disabled, variant }) => {
return (
<MarkToolbarButton
tooltip="Strikethrough"
type={MARK_STRIKETHROUGH}
variant={variant}
icon={FormatStrikethroughIcon}
disabled={disabled}
/>
);
};
export default StrikethroughToolbarButton;

View File

@ -1,104 +0,0 @@
import { TableAdd } from '@styled-icons/fluentui-system-regular/TableAdd';
import { TableDeleteColumn } from '@styled-icons/fluentui-system-regular/TableDeleteColumn';
import { TableDeleteRow } from '@styled-icons/fluentui-system-regular/TableDeleteRow';
import { TableDismiss } from '@styled-icons/fluentui-system-regular/TableDismiss';
import { TableInsertColumn } from '@styled-icons/fluentui-system-regular/TableInsertColumn';
import { TableInsertRow } from '@styled-icons/fluentui-system-regular/TableInsertRow';
import {
deleteColumn,
deleteRow,
deleteTable,
insertTable,
insertTableColumn,
insertTableRow,
} from '@udecode/plate';
import React, { useCallback } from 'react';
import ToolbarButton from './common/ToolbarButton';
import type { FC } from 'react';
import type { MdEditor } from '@staticcms/markdown';
export interface TableToolbarButtonsProps {
isInTable?: boolean;
disabled: boolean;
}
const TableToolbarButtons: FC<TableToolbarButtonsProps> = ({ isInTable = true, disabled }) => {
const handleTableAdd = useCallback((editor: MdEditor) => {
insertTable(editor, {
rowCount: 2,
colCount: 2,
});
}, []);
const handleInsertTableRow = useCallback((editor: MdEditor) => {
insertTableRow(editor);
}, []);
const handleDeleteRow = useCallback((editor: MdEditor) => {
deleteRow(editor);
}, []);
const handleInsertTableColumn = useCallback((editor: MdEditor) => {
insertTableColumn(editor);
}, []);
const handleDeleteColumn = useCallback((editor: MdEditor) => {
deleteColumn(editor);
}, []);
const handleDeleteTable = useCallback((editor: MdEditor) => {
deleteTable(editor);
}, []);
return isInTable ? (
<>
<ToolbarButton
key="insertRow"
tooltip="Insert Row"
icon={<TableInsertRow className="w-5 h-5" />}
onClick={handleInsertTableRow}
disabled={disabled}
/>
<ToolbarButton
key="deleteRow"
tooltip="Delete Row"
icon={<TableDeleteRow className="w-5 h-5" />}
onClick={handleDeleteRow}
disabled={disabled}
/>
<ToolbarButton
key="insertColumn"
tooltip="Insert Column"
icon={<TableInsertColumn className="w-5 h-5" />}
onClick={handleInsertTableColumn}
disabled={disabled}
/>
<ToolbarButton
key="deleteColumn"
tooltip="Delete Column"
icon={<TableDeleteColumn className="w-5 h-5" />}
onClick={handleDeleteColumn}
disabled={disabled}
/>
<ToolbarButton
key="deleteTable"
tooltip="Delete Table"
icon={<TableDismiss className="w-5 h-5" />}
onClick={handleDeleteTable}
disabled={disabled}
/>
</>
) : (
<ToolbarButton
key="insertRow"
tooltip="Add Table"
icon={<TableAdd className="w-5 h-5" />}
onClick={handleTableAdd}
disabled={disabled}
/>
);
};
export default TableToolbarButtons;

View File

@ -0,0 +1,26 @@
import { FormatListBulleted as FormatListBulletedIcon } from '@styled-icons/material/FormatListBulleted';
import { ELEMENT_UL } from '@udecode/plate';
import React from 'react';
import ListToolbarButton from './common/ListToolbarButton';
import type { FC } from 'react';
export interface UnorderedListToolbarButtonProps {
disabled: boolean;
variant: 'button' | 'menu';
}
const UnorderedListToolbarButton: FC<UnorderedListToolbarButtonProps> = ({ disabled, variant }) => {
return (
<ListToolbarButton
tooltip="List"
type={ELEMENT_UL}
icon={FormatListBulletedIcon}
disabled={disabled}
variant={variant}
/>
);
};
export default UnorderedListToolbarButton;

View File

@ -4,7 +4,6 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './ToolbarButton';
import type { MdEditor } from '@staticcms/markdown';
import type { Alignment } from '@udecode/plate';
import type { FC } from 'react';
import type { ToolbarButtonProps } from './ToolbarButton';
@ -12,6 +11,7 @@ import type { ToolbarButtonProps } from './ToolbarButton';
export interface AlignToolbarButtonProps extends Omit<ToolbarButtonProps, 'active' | 'onClick'> {
value: Alignment;
pluginKey?: string;
variant: 'button' | 'menu';
}
const AlignToolbarButton: FC<AlignToolbarButtonProps> = ({
@ -21,15 +21,12 @@ const AlignToolbarButton: FC<AlignToolbarButtonProps> = ({
}) => {
const editor = useMdPlateEditorState();
const handleOnClick = useCallback(
(editor: MdEditor) => {
setAlign(editor, {
value,
key: pluginKey,
});
},
[pluginKey, value],
);
const handleOnClick = useCallback(() => {
setAlign(editor, {
value,
key: pluginKey,
});
}, [editor, pluginKey, value]);
return (
<ToolbarButton

View File

@ -4,35 +4,33 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './ToolbarButton';
import type { MdEditor } from '@staticcms/markdown';
import type { FC } from 'react';
import type { ToolbarButtonProps } from './ToolbarButton';
export interface BlockToolbarButtonProps extends Omit<ToolbarButtonProps, 'active' | 'onClick'> {
type: string;
inactiveType?: string;
onClick?: (editor: MdEditor) => void;
variant: 'button' | 'menu';
}
const BlockToolbarButton: FC<BlockToolbarButtonProps> = ({
type,
inactiveType,
onClick,
icon,
...props
}) => {
const editor = useMdPlateEditorState();
const handleOnClick = useCallback(
(editor: MdEditor) => {
toggleNodeType(editor, { activeType: type, inactiveType });
},
[inactiveType, type],
);
const handleOnClick = useCallback(() => {
toggleNodeType(editor, { activeType: type, inactiveType });
}, [editor, inactiveType, type]);
return (
<ToolbarButton
key={type}
active={!!editor?.selection && someNode(editor, { match: { type } })}
onClick={onClick ?? handleOnClick}
onClick={handleOnClick}
icon={icon}
{...props}
/>
);

View File

@ -4,30 +4,32 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './ToolbarButton';
import type { MdEditor } from '@staticcms/markdown';
import type { FC } from 'react';
import type { ToolbarButtonProps } from './ToolbarButton';
export interface ListToolbarButtonProps extends Omit<ToolbarButtonProps, 'active' | 'onClick'> {
type: string;
variant: 'button' | 'menu';
}
const ListToolbarButton: FC<ListToolbarButtonProps> = ({ type, ...props }) => {
const ListToolbarButton: FC<ListToolbarButtonProps> = ({ type, icon, ...props }) => {
const editor = useMdPlateEditorState();
const handleOnClick = useCallback(
(editor: MdEditor) => {
toggleList(editor, {
type,
});
},
[type],
);
const handleOnClick = useCallback(() => {
toggleList(editor, {
type,
});
}, [editor, type]);
const res = !!editor?.selection && getListItemEntry(editor);
return (
<ToolbarButton active={!!res && res.list[0].type === type} onClick={handleOnClick} {...props} />
<ToolbarButton
active={!!res && res.list[0].type === type}
onClick={handleOnClick}
icon={icon}
{...props}
/>
);
};

View File

@ -4,29 +4,29 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './ToolbarButton';
import type { MdEditor } from '@staticcms/markdown';
import type { FC } from 'react';
import type { ToolbarButtonProps } from './ToolbarButton';
export interface MarkToolbarButtonProps extends Omit<ToolbarButtonProps, 'active' | 'onClick'> {
export interface MarkToolbarButtonProps
extends Omit<ToolbarButtonProps, 'active' | 'onClick' | 'icon'> {
type: string;
clear?: string | string[];
variant: 'button' | 'menu';
icon: FC<{ className?: string }>;
}
const MarkToolbarButton: FC<MarkToolbarButtonProps> = ({ type, clear, ...props }) => {
const MarkToolbarButton: FC<MarkToolbarButtonProps> = ({ type, clear, icon, ...props }) => {
const editor = useMdPlateEditorState();
const handleOnClick = useCallback(
(editor: MdEditor) => {
toggleMark(editor, { key: type, clear });
},
[clear, type],
);
const handleOnClick = useCallback(() => {
toggleMark(editor, { key: type, clear });
}, [clear, editor, type]);
return (
<ToolbarButton
active={!!editor?.selection && isMarkActive(editor, type)}
onClick={handleOnClick}
icon={icon}
{...props}
/>
);

View File

@ -2,44 +2,46 @@ import { focusEditor } from '@udecode/plate';
import React, { useCallback } from 'react';
import Button from '@staticcms/core/components/common/button/Button';
import MenuItemButton from '@staticcms/core/components/common/menu/MenuItemButton';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import type { MdEditor } from '@staticcms/markdown';
import type { CSSProperties, FC, MouseEvent, ReactNode } from 'react';
import type { CSSProperties, FC, MouseEvent } from 'react';
export interface ToolbarButtonProps {
label?: string;
tooltip: string;
active?: boolean;
activeColor?: string;
icon: ReactNode;
icon: FC<{ className?: string }>;
disableFocusAfterClick?: boolean;
disabled: boolean;
onClick: (editor: MdEditor, event: MouseEvent<HTMLButtonElement>) => void;
variant: 'button' | 'menu';
onClick: (event: MouseEvent) => void;
}
const ToolbarButton: FC<ToolbarButtonProps> = ({
icon,
icon: Icon,
tooltip,
label,
active = false,
activeColor,
disableFocusAfterClick = false,
disabled,
variant,
onClick,
}) => {
const editor = useMdPlateEditorState();
const handleOnClick = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
(event: MouseEvent) => {
event.preventDefault();
if (!editor) {
return;
}
onClick(editor, event);
onClick(event);
if (!disableFocusAfterClick) {
setTimeout(() => {
@ -55,8 +57,17 @@ const ToolbarButton: FC<ToolbarButtonProps> = ({
style.color = activeColor;
}
if (variant === 'menu') {
return (
<MenuItemButton key="menu-item" onClick={handleOnClick} startIcon={Icon}>
{label ?? tooltip}
</MenuItemButton>
);
}
return (
<Button
key="button"
aria-label={label ?? tooltip}
variant="text"
data-testid={`toolbar-button-${label ?? tooltip}`.replace(' ', '-').toLowerCase()}
@ -78,7 +89,7 @@ const ToolbarButton: FC<ToolbarButtonProps> = ({
style={style}
disabled={disabled}
>
{icon}
{<Icon className="w-5 h-5" />}
</Button>
);
};

View File

@ -5,8 +5,6 @@ export { default as BlockToolbarButton } from './BlockToolbarButton';
export * from './ColorPickerToolbarDropdown';
export { default as ColorPickerToolbarDropdown } from './ColorPickerToolbarDropdown';
export * from './dropdown';
export { default as ImageToolbarButton } from './ImageToolbarButton';
export { default as LinkToolbarButton } from './LinkToolbarButton';
export * from './ListToolbarButton';
export { default as ListToolbarButton } from './ListToolbarButton';
export * from './MarkToolbarButton';

View File

@ -1,14 +1,32 @@
export { default as AlignToolbarButtons } from './AlignToolbarButtons';
export * from './BasicElementToolbarButtons';
export { default as BasicElementToolbarButtons } from './BasicElementToolbarButtons';
export * from './BasicMarkToolbarButtons';
export { default as BasicMarkToolbarButtons } from './BasicMarkToolbarButtons';
export { default as ColorToolbarButtons } from './ColorToolbarButtons';
export * from './common';
export * from './BoldToolbarButton';
export { default as BoldToolbarButton } from './BoldToolbarButton';
export * from './DecreaseIndentToolbarButton';
export { default as IncreaseIndentToolbarButton } from './DecreaseIndentToolbarButton';
export * from './DeleteColumnToolbarButton';
export { default as DeleteColumnToolbarButton } from './DeleteColumnToolbarButton';
export * from './DeleteRowToolbarButton';
export { default as DeleteRowToolbarButton } from './DeleteRowToolbarButton';
export * from './DeleteTableToolbarButton';
export { default as DeleteTableToolbarButton } from './DeleteTableToolbarButton';
export * from './FontTypeSelect';
export { default as FontTypeSelect } from './FontTypeSelect';
export { default as ListToolbarButtons } from './ListToolbarButtons';
export * from './MediaToolbarButtons';
export { default as MediaToolbarButtons } from './MediaToolbarButtons';
export * from './TableToolbarButtons';
export { default as TableToolbarButtons } from './TableToolbarButtons';
export * from './InsertImageToolbarButton';
export { default as ImageToolbarButton } from './InsertImageToolbarButton';
export * from './IncreaseIndentToolbarButton';
export { default as DecreaseIndentToolbarButton } from './IncreaseIndentToolbarButton';
export * from './InsertColumnToolbarButton';
export { default as InsertColumnToolbarButton } from './InsertColumnToolbarButton';
export * from './InsertRowToolbarButton';
export { default as InsertRowToolbarButton } from './InsertRowToolbarButton';
export * from './ItalicToolbarButton';
export { default as ItalicToolbarButton } from './ItalicToolbarButton';
export * from './InsertLinkToolbarButton';
export { default as LinkToolbarButton } from './InsertLinkToolbarButton';
export * from './OrderedListToolbarButton';
export { default as OrderedListToolbarButton } from './OrderedListToolbarButton';
export * from './ShortcodeToolbarButton';
export { default as ShortcodeToolbarButton } from './ShortcodeToolbarButton';
export * from './StrikethroughToolbarButton';
export { default as StrikethroughToolbarButton } from './StrikethroughToolbarButton';
export * from './UnorderedListToolbarButton';
export { default as UnorderedListToolbarButton } from './UnorderedListToolbarButton';

View File

@ -1,16 +1,54 @@
import React from 'react';
import AddButtons from '../buttons/AddButtons';
import AlignToolbarButtons from '../buttons/AlignToolbarButtons';
import BasicElementToolbarButtons from '../buttons/BasicElementToolbarButtons';
import BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
import ColorToolbarButtons from '../buttons/ColorToolbarButtons';
import ListToolbarButtons from '../buttons/ListToolbarButtons';
import ShortcodeToolbarButton from '../buttons/ShortcodeToolbarButton';
import useToolbarButtons from '../../hooks/useToolbarButtons';
import {
BOLD_TOOLBAR_BUTTON,
ITALIC_TOOLBAR_BUTTON,
STRIKETHROUGH_TOOLBAR_BUTTON,
CODE_TOOLBAR_BUTTON,
FONT_TOOLBAR_BUTTON,
IMAGE_TOOLBAR_BUTTON,
FILE_LINK_TOOLBAR_BUTTON,
INSERT_TABLE_TOOLBAR_BUTTON,
BLOCKQUOTE_TOOLBAR_BUTTON,
SHORTCODE_TOOLBAR_BUTTON,
INCRASE_IDENT_TOOLBAR_BUTTON,
DECREASE_IDENT_TOOLBAR_BUTTON,
ORDERED_LIST_TOOLBAR_BUTTON,
UNORDERED_LIST_TOOLBAR_BUTTON,
CODE_BLOCK_TOOLBAR_BUTTON,
} from '@staticcms/core/constants/toolbar_buttons';
import type { Collection, MarkdownField } from '@staticcms/core/interface';
import type { Collection, MarkdownField, MarkdownToolbarItem } from '@staticcms/core/interface';
import type { FC } from 'react';
const DEFAULT_TOOLBAR_BUTTONS: MarkdownToolbarItem[] = [
BOLD_TOOLBAR_BUTTON,
ITALIC_TOOLBAR_BUTTON,
STRIKETHROUGH_TOOLBAR_BUTTON,
CODE_TOOLBAR_BUTTON,
FONT_TOOLBAR_BUTTON,
UNORDERED_LIST_TOOLBAR_BUTTON,
ORDERED_LIST_TOOLBAR_BUTTON,
DECREASE_IDENT_TOOLBAR_BUTTON,
INCRASE_IDENT_TOOLBAR_BUTTON,
SHORTCODE_TOOLBAR_BUTTON,
{
label: 'Insert',
groups: [
{
items: [BLOCKQUOTE_TOOLBAR_BUTTON, CODE_BLOCK_TOOLBAR_BUTTON],
},
{
items: [INSERT_TABLE_TOOLBAR_BUTTON],
},
{
items: [IMAGE_TOOLBAR_BUTTON, FILE_LINK_TOOLBAR_BUTTON],
},
],
},
];
export interface ToolbarProps {
useMdx: boolean;
collection: Collection<MarkdownField>;
@ -18,21 +56,13 @@ export interface ToolbarProps {
disabled: boolean;
}
const Toolbar: FC<ToolbarProps> = ({ useMdx, collection, field, disabled }) => {
const groups = [
<BasicMarkToolbarButtons
key="basic-mark-buttons"
useMdx={useMdx}
extended
disabled={disabled}
/>,
<BasicElementToolbarButtons key="basic-element-buttons" disabled={disabled} />,
<ListToolbarButtons key="list-buttons" disabled={disabled} />,
useMdx ? <ColorToolbarButtons key="color-buttons" disabled={disabled} /> : null,
useMdx ? <AlignToolbarButtons key="align-mark-buttons" disabled={disabled} /> : null,
!useMdx ? <ShortcodeToolbarButton key="shortcode-button" disabled={disabled} /> : null,
<AddButtons key="add-buttons" collection={collection} field={field} disabled={disabled} />,
].filter(Boolean);
const Toolbar: FC<ToolbarProps> = ({ collection, field, disabled }) => {
const buttons = useToolbarButtons(
field.toolbar_buttons?.main ?? DEFAULT_TOOLBAR_BUTTONS,
collection,
field,
disabled,
);
return (
<div
@ -59,7 +89,7 @@ const Toolbar: FC<ToolbarProps> = ({ useMdx, collection, field, disabled }) => {
z-10
"
>
{groups}
{buttons}
</div>
);
};

View File

@ -0,0 +1,206 @@
import { Add as AddIcon } from '@styled-icons/material/Add';
import React, { useMemo } from 'react';
import Menu from '@staticcms/core/components/common/menu/Menu';
import MenuGroup from '@staticcms/core/components/common/menu/MenuGroup';
import BlockquoteToolbarButton from '../components/buttons/BlockquoteToolbarButton';
import BoldToolbarButton from '../components/buttons/BoldToolbarButton';
import CodeBlockToolbarButtons from '../components/buttons/CodeBlockToolbarButtons';
import CodeToolbarButton from '../components/buttons/CodeToolbarButton';
import IncreaseIndentButton from '../components/buttons/DecreaseIndentToolbarButton';
import DeleteColumnToolbarButton from '../components/buttons/DeleteColumnToolbarButton';
import DeleteRowToolbarButton from '../components/buttons/DeleteRowToolbarButton';
import DeleteTableToolbarButton from '../components/buttons/DeleteTableToolbarButton';
import FontTypeSelect from '../components/buttons/FontTypeSelect';
import DecreaseIndentButton from '../components/buttons/IncreaseIndentToolbarButton';
import InsertColumnToolbarButton from '../components/buttons/InsertColumnToolbarButton';
import InsertImageToolbarButton from '../components/buttons/InsertImageToolbarButton';
import InsertLinkToolbarButton from '../components/buttons/InsertLinkToolbarButton';
import InsertRowToolbarButton from '../components/buttons/InsertRowToolbarButton';
import InsertTableToolbarButton from '../components/buttons/InsertTableToolbarButton';
import ItalicToolbarButton from '../components/buttons/ItalicToolbarButton';
import OrderedListButton from '../components/buttons/OrderedListToolbarButton';
import ShortcodeToolbarButton from '../components/buttons/ShortcodeToolbarButton';
import StrikethroughToolbarButton from '../components/buttons/StrikethroughToolbarButton';
import UnorderedListButton from '../components/buttons/UnorderedListToolbarButton';
import type {
Collection,
LowLevelMarkdownToolbarButtonType,
MarkdownField,
MarkdownToolbarButtonType,
MarkdownToolbarItem,
} from '@staticcms/core/interface';
import type { ReactNode } from 'react';
export default function useToolbarButtons(
toolbarButtons: MarkdownToolbarItem[],
collection: Collection<MarkdownField>,
field: MarkdownField,
disabled: boolean,
): ReactNode[] {
return useMemo(
() => getToolbarButtons(toolbarButtons, collection, field, disabled),
[collection, disabled, field, toolbarButtons],
);
}
export function getToolbarButtons(
toolbarButtons: MarkdownToolbarItem[] | MarkdownToolbarButtonType[],
collection: Collection<MarkdownField>,
field: MarkdownField,
disabled: boolean,
): ReactNode[] {
return toolbarButtons.map(button => {
if (typeof button === 'string') {
return getToolbarButton(button, collection, field, disabled, 'button');
}
return (
<Menu
key={`menu-${button.label}`}
label={<AddIcon className="h-5 w-5" aria-hidden="true" />}
data-testid={`toolbar-menu-${button.label.toLowerCase().replace(' ', '-')}`}
keepMounted
hideDropdownIcon
variant="text"
className="
py-0.5
px-0.5
h-6
w-6
"
disabled={disabled}
>
{button.groups.map((group, index) => {
if (group.items.length === 0) {
return null;
}
return (
<MenuGroup key={`group-${index}`}>
{group.items.map(item => getToolbarButton(item, collection, field, disabled, 'menu'))}
</MenuGroup>
);
})}
</Menu>
);
});
}
function getToolbarButton(
name: MarkdownToolbarButtonType,
collection: Collection<MarkdownField>,
field: MarkdownField,
disabled: boolean,
variant: 'button',
): ReactNode;
function getToolbarButton(
name: LowLevelMarkdownToolbarButtonType,
collection: Collection<MarkdownField>,
field: MarkdownField,
disabled: boolean,
variant: 'menu',
): ReactNode;
function getToolbarButton(
name: MarkdownToolbarButtonType | LowLevelMarkdownToolbarButtonType,
collection: Collection<MarkdownField>,
field: MarkdownField,
disabled: boolean,
variant: 'button' | 'menu',
): ReactNode {
switch (name) {
case 'blockquote':
return <BlockquoteToolbarButton key="bold" disabled={disabled} variant={variant} />;
case 'bold':
return <BoldToolbarButton key="bold" disabled={disabled} variant={variant} />;
case 'code':
return <CodeToolbarButton key="code" disabled={disabled} variant={variant} />;
case 'code-block':
return <CodeBlockToolbarButtons key="code" disabled={disabled} variant={variant} />;
case 'decrease-indent':
return <DecreaseIndentButton key="decrease-indent" disabled={disabled} variant={variant} />;
case 'delete-column':
return (
<DeleteColumnToolbarButton key="delete-column" disabled={disabled} variant={variant} />
);
case 'delete-row':
return <DeleteRowToolbarButton key="delete-row" disabled={disabled} variant={variant} />;
case 'delete-table':
return <DeleteTableToolbarButton key="delete-table" disabled={disabled} variant={variant} />;
case 'font':
if (variant === 'menu') {
return null;
}
return <FontTypeSelect key="font" disabled={disabled} />;
case 'increase-indent':
return <IncreaseIndentButton key="increase-indent" disabled={disabled} variant={variant} />;
case 'insert-column':
return (
<InsertColumnToolbarButton key="insert-column" disabled={disabled} variant={variant} />
);
case 'image':
return (
<InsertImageToolbarButton
key="image"
disabled={disabled}
variant={variant}
collection={collection}
field={field}
/>
);
case 'file-link':
return (
<InsertLinkToolbarButton
key="file-link"
disabled={disabled}
variant={variant}
collection={collection}
field={field}
/>
);
case 'insert-row':
return <InsertRowToolbarButton key="insert-row" disabled={disabled} variant={variant} />;
case 'insert-table':
return <InsertTableToolbarButton key="insert-table" disabled={disabled} variant={variant} />;
case 'italic':
return <ItalicToolbarButton key="italic" disabled={disabled} variant={variant} />;
case 'ordered-list':
return <OrderedListButton key="ordered-list" disabled={disabled} variant={variant} />;
case 'shortcode':
if (variant === 'menu') {
return null;
}
return <ShortcodeToolbarButton key="shortcode" disabled={disabled} />;
case 'strikethrough':
return (
<StrikethroughToolbarButton key="strikethrough" disabled={disabled} variant={variant} />
);
case 'unordered-list':
return <UnorderedListButton key="unordered-list" disabled={disabled} variant={variant} />;
default:
return null;
}
}

View File

@ -1,3 +1,104 @@
import {
FONT_TOOLBAR_BUTTON,
SHORTCODE_TOOLBAR_BUTTON,
BLOCKQUOTE_TOOLBAR_BUTTON,
BOLD_TOOLBAR_BUTTON,
CODE_BLOCK_TOOLBAR_BUTTON,
CODE_TOOLBAR_BUTTON,
DECREASE_IDENT_TOOLBAR_BUTTON,
DELETE_COLUMN_TOOLBAR_BUTTON,
DELETE_ROW_TOOLBAR_BUTTON,
DELETE_TABLE_TOOLBAR_BUTTON,
INCRASE_IDENT_TOOLBAR_BUTTON,
INSERT_COLUMN_TOOLBAR_BUTTON,
IMAGE_TOOLBAR_BUTTON,
FILE_LINK_TOOLBAR_BUTTON,
INSERT_ROW_TOOLBAR_BUTTON,
INSERT_TABLE_TOOLBAR_BUTTON,
ITALIC_TOOLBAR_BUTTON,
ORDERED_LIST_TOOLBAR_BUTTON,
STRIKETHROUGH_TOOLBAR_BUTTON,
UNORDERED_LIST_TOOLBAR_BUTTON,
} from '@staticcms/core/constants/toolbar_buttons';
const LowLevelButtonsArrayField = {
type: 'array',
items: {
type: 'string',
enum: [
BLOCKQUOTE_TOOLBAR_BUTTON,
BOLD_TOOLBAR_BUTTON,
CODE_BLOCK_TOOLBAR_BUTTON,
CODE_TOOLBAR_BUTTON,
DECREASE_IDENT_TOOLBAR_BUTTON,
DELETE_COLUMN_TOOLBAR_BUTTON,
DELETE_ROW_TOOLBAR_BUTTON,
DELETE_TABLE_TOOLBAR_BUTTON,
INCRASE_IDENT_TOOLBAR_BUTTON,
INSERT_COLUMN_TOOLBAR_BUTTON,
IMAGE_TOOLBAR_BUTTON,
FILE_LINK_TOOLBAR_BUTTON,
INSERT_ROW_TOOLBAR_BUTTON,
INSERT_TABLE_TOOLBAR_BUTTON,
ITALIC_TOOLBAR_BUTTON,
ORDERED_LIST_TOOLBAR_BUTTON,
STRIKETHROUGH_TOOLBAR_BUTTON,
UNORDERED_LIST_TOOLBAR_BUTTON,
],
},
};
const MarkdownToolbarItemField = {
type: 'array',
items: {
anyOf: [
{
type: 'string',
enum: [
FONT_TOOLBAR_BUTTON,
SHORTCODE_TOOLBAR_BUTTON,
BLOCKQUOTE_TOOLBAR_BUTTON,
BOLD_TOOLBAR_BUTTON,
CODE_BLOCK_TOOLBAR_BUTTON,
CODE_TOOLBAR_BUTTON,
DECREASE_IDENT_TOOLBAR_BUTTON,
DELETE_COLUMN_TOOLBAR_BUTTON,
DELETE_ROW_TOOLBAR_BUTTON,
DELETE_TABLE_TOOLBAR_BUTTON,
INCRASE_IDENT_TOOLBAR_BUTTON,
INSERT_COLUMN_TOOLBAR_BUTTON,
IMAGE_TOOLBAR_BUTTON,
FILE_LINK_TOOLBAR_BUTTON,
INSERT_ROW_TOOLBAR_BUTTON,
INSERT_TABLE_TOOLBAR_BUTTON,
ITALIC_TOOLBAR_BUTTON,
ORDERED_LIST_TOOLBAR_BUTTON,
STRIKETHROUGH_TOOLBAR_BUTTON,
UNORDERED_LIST_TOOLBAR_BUTTON,
],
},
{
type: 'object',
properties: {
label: { type: 'string' },
icon: { type: 'string' },
groups: {
type: 'array',
items: {
type: 'object',
properties: {
items: LowLevelButtonsArrayField,
},
required: ['items'],
},
},
},
required: ['label', 'groups'],
},
],
},
};
export default {
properties: {
default: { type: 'string' },
@ -5,6 +106,16 @@ export default {
public_folder: { type: 'string' },
choose_url: { type: 'boolean' },
multiple: { type: 'boolean' },
toolbar_buttons: {
type: 'object',
properties: {
main: MarkdownToolbarItemField,
empty: MarkdownToolbarItemField,
selection: MarkdownToolbarItemField,
table_empty: MarkdownToolbarItemField,
table_select: MarkdownToolbarItemField,
},
},
media_library: {
type: 'object',
properties: {

View File

@ -16,13 +16,14 @@ _Please note:_ If you want to use your markdown editor to fill a markdown file c
For common options, see [Common widget options](/docs/widgets#common-widget-options).
| Name | Type | Default | Description |
| ------------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| default | string | `''` | _Optional_. The default value for the field. Accepts markdown content |
| media_folder | string | | _Optional_. Specifies the folder path where uploaded files should be saved, relative to the base of the repo |
| public_folder | string | | _Optional_. Specifies the folder path where the files uploaded by the media library will be accessed, relative to the base of the built site |
| media_library | Media Library Options | `{}` | _Optional_. Media library settings to apply when the media library is opened by the current widget. See [Media Library](/docs/configuration-options#media-library) |
| choose_url | boolean | `true` | _Optional_. When set to `false`, the "Insert from URL" button will be hidden |
| Name | Type | Default | Description |
| --------------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| default | string | `''` | _Optional_. The default value for the field. Accepts markdown content |
| media_folder | string | | _Optional_. Specifies the folder path where uploaded files should be saved, relative to the base of the repo |
| public_folder | string | | _Optional_. Specifies the folder path where the files uploaded by the media library will be accessed, relative to the base of the built site |
| media_library | Media Library Options | `{}` | _Optional_. Media library settings to apply when the media library is opened by the current widget. See [Media Library](/docs/configuration-options#media-library) |
| choose_url | boolean | `true` | _Optional_. When set to `false`, the "Insert from URL" button will be hidden |
| toolbar_buttons | object | [] | _Optional_. Specifies which toolbar items to show for the markdown widget. See [Toolbar Customization](#toolbar-customization) |
## Example
@ -47,6 +48,234 @@ This would render as:
_Please note:_ The markdown widget outputs a raw markdown string. Your static site generator may or may not render the markdown to HTML automatically. Consult with your static site generator's documentation for more information about rendering markdown.
## Toolbar Customization
| Name | Type | Default | Description |
| --------------- | ------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| main | object | See [Main Toolbar](#main-toolbar) | _Optional_. A list of buttons and menus for the top level toolbar |
| empty | object | See [Empty Toolbar](#empty-toolbar) | _Optional_. A list of buttons and menus for the popup toolbar when in an empty paragraph |
| selection | object | See [Selection Toolbar](#selection-toolbar) | _Optional_. A list of buttons and menus for the popup toolbar when selecting text outside of a table cell |
| table_empty | object | See [Empty Table Cell Toolbar](#empty-table-cell-toolbar) | _Optional_. A list of buttons and menus for the popup toolbar when in an empty table cell |
| table_selection | object | See [Table Selection Toolbar](#table-selection-toolbar) | _Optional_. A list of buttons and menus for the popup toolbar when selecting text in a table cell |
### Options
All toolbars can be customized with a list consisting of buttons and dropdown menus.
#### Buttons
Buttons can be configured simply with their name. The following options are available:
- `blockquote`
- `bold`
- `code`
- `code-block`
- `decrease-indent`
- `delete-column`
- `delete-row`
- `delete-table`
- `file-link`
- `font`
- `image`
- `increase-indent`
- `insert-column`
- `insert-row`
- `insert-table`
- `italic`
- `ordered-list`
- `shortcode`
- `strikethrough`
- `unordered-list`
#### Menus
The following options are available for menus:
| Name | Type | Description |
| ------ | -------------- | ----------------------------------------------------------------------------------------- |
| label | string | The name and tooltip label for the menu |
| icon | string | _Optional_. The icon to use for the menu. If not supplied a default add icon will be used |
| groups | list of groups | A list groups of menu items. Each group is separated by a divider |
##### Menu Groups
The following options are available for menu groups:
| Name | Type | Description |
| ----- | --------------- | -------------------------------------------------------------------------------------------------------------------------- |
| items | list of strings | The name of the toolbar buttons in the group. All [buttons](#buttons) are available in menus except `font` and `shortcode` |
### Default Values
Below are the default values for the various toolbars:
#### Main Toolbar
<CodeTabs>
```yaml
main:
- bold
- italic
- strikethrough
- code
- font
- unordered-list
- ordered-list
- decrease-indent
- increase-indent
- shortcode
- label: Insert
groups:
- items:
- blockquote
- code-block
- items:
- insert-table
- items:
- image
- file-link
```
```js
main: [
'bold',
'italic',
'strikethrough',
'code',
'font',
'unordered-list',
'ordered-list',
'decrease-indent',
'increase-indent',
'shortcode',
{
label: 'Insert',
groups: [
{
items: ['blockquote', 'code-block'],
},
{
items: ['insert-table'],
},
{
items: ['image', 'file-link'],
},
],
},
];
```
</CodeTabs>
#### Empty Toolbar
<CodeTabs>
```yaml
empty: []
```
```js
empty: [];
```
</CodeTabs>
#### Selection Toolbar
<CodeTabs>
```yaml
selection:
- bold
- italic
- strikethrough
- code
- font
- file-link
```
```js
selection: ['bold', 'italic', 'strikethrough', 'code', 'font', 'file-link'];
```
</CodeTabs>
#### Empty Table Cell Toolbar
<CodeTabs>
```yaml
table_empty:
- bold
- italic
- strikethrough
- code
- insert-row
- delete-row
- insert-column
- delete-column
- delete-table
- file-link
- image
- shortcode
```
```js
table_empty: [
'bold',
'italic',
'strikethrough',
'code',
'insert-row',
'delete-row',
'insert-column',
'delete-column',
'delete-table',
'file-link',
'image',
'shortcode',
];
```
</CodeTabs>
#### Table Selection Toolbar
<CodeTabs>
```yaml
table_selection:
- bold
- italic
- strikethrough
- code
- insert-row
- delete-row
- insert-column
- delete-column
- delete-table
- file-link
```
```js
table_selection: [
'bold',
'italic',
'strikethrough',
'code',
'insert-row',
'delete-row',
'insert-column',
'delete-column',
'delete-table',
'file-link',
];
```
</CodeTabs>
## Shortcodes
Shortcodes can be added to customize the Markdown editor via `registerShortcode`.

View File

@ -36,6 +36,7 @@ const Anchor = ({ href = '', children = '' }: AnchorProps) => {
document.querySelector(href)?.scrollIntoView({
behavior: 'smooth',
});
history.pushState(null, '', href);
}}
>
{children}

View File

@ -39,6 +39,7 @@ const Header3 = ({ variant, children = '' }: Header3Props) => {
const hasText = useMemo(() => isNotEmpty(textContent), [textContent]);
return (
<Typography
id={anchor}
variant={variant}
component={variant}
sx={{

View File

@ -8,7 +8,7 @@ const StyledList = styled('ul')(
flex-direction: column;
list-style-type: none;
padding: 0;
${theme.breakpoints.down('lg')} {
margin-top: 0;
}
@ -64,6 +64,7 @@ const DocsHeadings = ({ headings, activeId }: DocsHeadingsProps) => (
document.querySelector(`#${heading.id}`)?.scrollIntoView({
behavior: 'smooth',
});
history.pushState(null, '', `#${heading.id}`);
}}
>
{heading.title}
@ -79,6 +80,7 @@ const DocsHeadings = ({ headings, activeId }: DocsHeadingsProps) => (
document.querySelector(`#${child.id}`)?.scrollIntoView({
behavior: 'smooth',
});
history.pushState(null, '', `#${child.id}`);
}}
>
{child.title}