RTE Text Alignment findings & assistance

default discord avatar
markatomniuxlast year
7

Hey folks, I've been trying to build text alignment into my RTE (like I'm sure a lot of you have tried to do as well) and I'm wanting to share some of the stuff I've done so far. Just a disclaimer, this is currently working in a very limited capacity, the UI is glitchy, and the only thing is achieves so far is mutating the target node to have alignment as an option. I have trawled through every CMS project on the Payload Github to identify key areas for developing a custom text alignment element. Here are my findings so far;



- Text Alignment should be done using an Element rather than a Leaf, this is because leafs alter their child elements directly, whereas Elements affect a block. So if you have a h4 tag with Bold and Underline leaves, you want to add alignment to the entire text block rather than the normal text, bold and underline leaves.


- Text Alignment should not be treated as a unique element, it is an extension of the current node. According to this post -

https://news.ycombinator.com/item?id=14127632

- the author of Slate states that you should add custom data to the nodes, rather than setting the 'type' property of the node (like what happens currently when applying h1, h2, h3...)


- When attempting to use Slatejs or Slate-react directly inside Element or Button code, YOU DO NOT NEED TO NPM INSTALL THESE PACKAGES. Instead, using the version used by payload. These packages can be accessed from

payload/node_modules/slate

and

payload/node_modules/slate-react


And now, my code;



import React, { useCallback } from 'react';
import { ElementButton } from 'payload/components/rich-text';
import { useSlate, ReactEditor } from 'payload/node_modules/slate-react'
import { Transforms, Editor, Element as SlateElement } from 'payload/node_modules/slate'
import { IconAlignCenter, IconAlignLeft, IconAlignRight } from '../Icons';

const TEXT_ALIGN_TYPES = ['left', 'center', 'right']

type AlignmentNode = Partial<SlateElement &
{
    align?: typeof TEXT_ALIGN_TYPES,
    type: string
}>

const addAlignment = (editor, format) => {

    let targetNode: AlignmentNode = undefined;

    Transforms.unwrapNodes(editor, {
        match: n => {
            var match = !Editor.isEditor(n) &&
                SlateElement.isElement(n) &&
                !TEXT_ALIGN_TYPES.includes(format)

            if (match) {
                targetNode = n as AlignmentNode;
                return true;
            }
            return false;
        },
        split: true,
    })

    let newProperties: AlignmentNode = targetNode

    newProperties = {
        align: format,
    }

    Transforms.setNodes<SlateElement>(editor, newProperties)

    ReactEditor.focus(editor);
};

const Button: React.FC<{ path: string }> = () => {

    const editor = useSlate();

    return (<>
        <ElementButton onClick={useCallback(() => addAlignment(editor, 'left'), [editor])}
            tooltip='left'
            format='left'
        >
            <IconAlignLeft />
        </ElementButton>

        <ElementButton onClick={useCallback(() => addAlignment(editor, 'center'), [editor])}
            tooltip='center'
            format='center'
        >
            <IconAlignCenter />
        </ElementButton>

        <ElementButton onClick={useCallback(() => addAlignment(editor, 'right'), [editor])}
            tooltip='right'
            format='right'
        >
            <IconAlignRight />
        </ElementButton>
    </>
    );
}

export default Button


- Does this work?



Yeeeeaahhmmhmm... It works in such a way that it adds a custom node to the top level element. It adds an alignment field when it's applied; either left, right, or center. It doesn't actually change anything in the Editor UI as for some reason the element re-render doesn't seem to get triggered. Because it doesn't re-trigger, I am unable to actually make changes to way the text is displayed on the editor. HOWEVER, rest assured that the change has actually happened, so json object passed to your website front-end WILL show this change (big win). Obviously this isn't workable from a general user perspective as the UI appears to be unresponsive in Payload. But it's a start!



So guys, I have a baseline. Who wants to help improve it so we can actually show this in the Payload UI?

  • discord user avatar
    seanzubrickas
    last year

    Hey Mark - this is certainly something that should be on our roadmap if it isn't already. I'll get some more answers on this one for you shortly. Stay tuned!

  • default discord avatar
    markatomniuxlast year

    No worries @seanzubrickas 🙂 I saw that 1.6 released with a new toggleElement function, that might be the thing that solves the button toggle issue. If that's the case I'll share the updated code

  • default discord avatar
    shooreh.pippilast year

    hi @markatomniux , did you managed to work on the updated code with the toggleElement function?

  • default discord avatar
    markatomniuxlast year

    Hi @shooreh.pippi When I checked toggleElement, it was fixed with 2 values and didn't allow me to pass custom node elements (align). That was a while ago though so it's maybe changed since then!



    I've been looking further into the toggle Element function and I believe I can intercept it and create a new element that wraps around your other elements. I can then attempt to flatten this down so that the element's format is removed, replaced with a new "alignment" field. This might or might not still satisfy the post-condition for the toggleElement function, however if it doesn't then I will PR a change to the toggleElement function to allow for custom fields

  • default discord avatar
    shooreh.pippilast year

    i wrote a really simple implementation like this:


    import { ElementButton } from "payload/components/rich-text";
    import React from "react";
    
    const Button = () => {
      return <ElementButton format="center">C</ElementButton>;
    };
    
    const Element: React.FC<{
      children: React.ReactNode;
      attributes: any;
      element: any;
    }> = ({ attributes, children }) => {
      return (
        <div {...attributes} style={{ textAlign: "center" }}>
          {children}
        </div>
      );
    };
    
    export default {
      name: "center",
      Button,
      Element,
    };

    and on the front-end side, the data is received with

    type: center

    , which allows me to style the text alignment accordingly. i use the

    slate-serializers

    package and pass this to

    elementTransforms

    :


    center: ({ children }) => {
      return new Element("p", { style: "text-align: center;" }, children);
    },

    repeating the same code for right and left alignment. probably won't work for all use cases, but works well enough for mine.

  • default discord avatar
    markatomniuxlast year

    Does this work with text that have other element types associated? For instance a H1 tag?



    I've opened a PR with a change that should allow me to implement a much more sophisticated Text Alignment solution with little additional slate code. If the PR gets approved, I'll push to have it included in Payload's default Slate editor

  • default discord avatar
    _bohuslavlast year

    Nice, when will this be available in payload default editor?

Star on GitHub

Star

Chat on Discord

Discord

online

Can't find what you're looking for?

Get help straight from the Payload team with an Enterprise License.