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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
@@ -6,5 +6,6 @@ export type ButtonProps = {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
tooltip?: string
|
||||
type?: string
|
||||
el?: ElementType
|
||||
}
|
||||
|
||||
14
src/admin/components/icons/AlignCenter/index.tsx
Normal file
14
src/admin/components/icons/AlignCenter/index.tsx
Normal 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;
|
||||
14
src/admin/components/icons/AlignLeft/index.tsx
Normal file
14
src/admin/components/icons/AlignLeft/index.tsx
Normal 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;
|
||||
14
src/admin/components/icons/AlignRight/index.tsx
Normal file
14
src/admin/components/icons/AlignRight/index.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user