feat: text alignment for richtext editor (#2803)

* Update isActive.tsx

This change allows us to define toggling of custom types in Slate. Specifically, this fixes the ability to toggle Alignment on nodes that use other active elements.

isElementActive(editor, format, TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type');

Type is the default for elements, allowing us to use a custom field lets us greater extend the functionality of Slate in Payload without causing any breaking changes

* Update toggle.tsx

Added to toggleElement public function

* Update isActive.tsx

* Update toggle.tsx

Added Rich Text Alignment, updated toggle function, added tests and doc updates

* added margin to void elements

* fix: list alignment

* removed textAlign from elements and added docs

* chore: fix typo

---------

Co-authored-by: Alessio Gravili <alessio@gravili.de>
This commit is contained in:
Mark Barton
2023-08-14 16:08:50 +01:00
committed by GitHub
parent 5744de7ec6
commit a0b13a5b01
15 changed files with 302 additions and 181 deletions

View File

@@ -77,13 +77,50 @@ const RichText: React.FC<Props> = (props) => {
const drawerIsOpen = drawerDepth > 1;
const renderElement = useCallback(({ attributes, children, element }) => {
const matchedElement = enabledElements[element?.type];
const matchedElement = enabledElements[element.type];
const Element = matchedElement?.Element;
let attr = { ...attributes };
// this converts text alignment to margin when dealing with void elements
if (element.textAlign) {
if (element.type === 'relationship' || element.type === 'upload') {
switch (element.textAlign) {
case 'left':
attr = { ...attr, style: { marginRight: 'auto' } };
break;
case 'right':
attr = { ...attr, style: { marginLeft: 'auto' } };
break;
case 'center':
attr = { ...attr, style: { marginLeft: 'auto', marginRight: 'auto' } };
break;
default:
attr = { ...attr, style: { textAlign: element.textAlign } };
break;
}
} else if (element.type === 'li') {
switch (element.textAlign) {
case 'right':
attr = { ...attr, style: { textAlign: 'right', listStylePosition: 'inside' } };
break;
case 'center':
attr = { ...attr, style: { textAlign: 'center', listStylePosition: 'inside' } };
break;
case 'left':
default:
attr = { ...attr, style: { textAlign: 'left', listStylePosition: 'outside' } };
break;
}
} else {
attr = { ...attr, style: { textAlign: element.textAlign } };
}
}
if (Element) {
return (
const el = (
<Element
attributes={attributes}
attributes={attr}
element={element}
path={path}
fieldProps={props}
@@ -92,9 +129,17 @@ const RichText: React.FC<Props> = (props) => {
{children}
</Element>
);
return el;
}
return <div {...attributes}>{children}</div>;
return (
<div
{...attr}
>
{children}
</div>
);
}, [enabledElements, path, props]);
const renderLeaf = useCallback(({ attributes, children, leaf }) => {
@@ -163,7 +208,6 @@ const RichText: React.FC<Props> = (props) => {
);
CreatedEditor = withHTML(CreatedEditor);
CreatedEditor = enablePlugins(CreatedEditor, elements);
CreatedEditor = enablePlugins(CreatedEditor, leaves);

View File

@@ -16,6 +16,7 @@ const ElementButton: React.FC<ButtonProps> = (props) => {
onClick,
className,
tooltip,
type = 'type',
el = 'button',
} = props;
@@ -25,8 +26,8 @@ const ElementButton: React.FC<ButtonProps> = (props) => {
const defaultOnClick = useCallback((event) => {
event.preventDefault();
setShowTooltip(false);
toggleElement(editor, format);
}, [editor, format]);
toggleElement(editor, format, type);
}, [editor, format, type]);
const Tag: ElementType = el;
@@ -36,7 +37,7 @@ const ElementButton: React.FC<ButtonProps> = (props) => {
className={[
baseClass,
className,
isElementActive(editor, format) && `${baseClass}__button--active`,
isElementActive(editor, format, type) && `${baseClass}__button--active`,
].filter(Boolean).join(' ')}
onClick={onClick || defaultOnClick}
onMouseEnter={() => setShowTooltip(true)}

View File

@@ -12,6 +12,7 @@ import li from './li';
import indent from './indent';
import relationship from './relationship';
import upload from './upload';
import textAlign from './textAlign';
const elements = {
h1,
@@ -25,6 +26,7 @@ const elements = {
ol,
ul,
li,
textAlign,
indent,
relationship,
upload,

View File

@@ -1,11 +1,11 @@
import { Editor, Element } from 'slate';
const isElementActive = (editor: Editor, format: string): boolean => {
const isElementActive = (editor: Editor, format: string, blockType = 'type'): boolean => {
if (!editor.selection) return false;
const [match] = Array.from(Editor.nodes(editor, {
at: Editor.unhangRange(editor, editor.selection),
match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === format,
match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n[blockType] === format,
}));
return !!match;

View File

@@ -7,7 +7,7 @@ const LI = (props) => {
return (
<li
style={{ listStyle: disableListStyle ? 'none' : undefined }}
style={{ listStyle: disableListStyle ? 'none' : undefined, listStylePosition: disableListStyle ? 'outside' : undefined }}
{...attributes}
>
{children}

View File

@@ -0,0 +1,33 @@
import React from 'react';
import ElementButton from '../Button';
import AlignLeftIcon from '../../../../../icons/AlignLeft';
import AlignCenterIcon from '../../../../../icons/AlignCenter';
import AlignRightIcon from '../../../../../icons/AlignRight';
export default {
name: 'alignment',
Button: () => {
return (
<React.Fragment>
<ElementButton
format="left"
type="textAlign"
>
<AlignLeftIcon />
</ElementButton>
<ElementButton
format="center"
type="textAlign"
>
<AlignCenterIcon />
</ElementButton>
<ElementButton
format="right"
type="textAlign"
>
<AlignRightIcon />
</ElementButton>
</React.Fragment>
);
},
};

View File

@@ -3,24 +3,27 @@ import { ReactEditor } from 'slate-react';
import isElementActive from './isActive';
import { isWithinListItem } from './isWithinListItem';
const toggleElement = (editor: Editor, format: string): void => {
const isActive = isElementActive(editor, format);
let type = format;
const toggleElement = (editor: Editor, format: string, blockType = 'type'): void => {
const isActive = isElementActive(editor, format, blockType);
const formatByBlockType = {
[blockType]: format,
};
const isWithinLI = isWithinListItem(editor);
if (isActive) {
type = undefined;
formatByBlockType[blockType] = undefined;
}
if (!isActive && isWithinLI) {
if (!isActive && isWithinLI && blockType !== 'textAlign') {
const block = { type: 'li', children: [] };
Transforms.wrapNodes(editor, block, {
at: Editor.unhangRange(editor, editor.selection),
});
}
Transforms.setNodes(editor, { type }, {
Transforms.setNodes(editor, { [blockType]: formatByBlockType[blockType] }, {
at: Editor.unhangRange(editor, editor.selection),
});

View File

@@ -6,5 +6,6 @@ export type ButtonProps = {
className?: string
children?: React.ReactNode
tooltip?: string
type?: string
el?: ElementType
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
const AlignCenterIcon: React.FC = () => (
<svg
viewBox="0 0 1024 1024"
fill="currentColor"
height="1em"
width="1em"
>
<path d="M264 230h496c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H264c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm496 424c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H264c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496zm144 140H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-424H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z" />
</svg>
);
export default AlignCenterIcon;

View File

@@ -0,0 +1,14 @@
import React from 'react';
const AlignLeftIcon: React.FC = () => (
<svg
viewBox="0 0 1024 1024"
fill="currentColor"
height="1em"
width="1em"
>
<path d="M120 230h496c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm0 424h496c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm784 140H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-424H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z" />
</svg>
);
export default AlignLeftIcon;

View File

@@ -0,0 +1,14 @@
import React from 'react';
const AlignRightIcon: React.FC = () => (
<svg
viewBox="0 0 1024 1024"
fill="currentColor"
height="1em"
width="1em"
>
<path d="M904 158H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 424H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 212H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-424H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z" />
</svg>
);
export default AlignRightIcon;