Add rich-text support with WYSIWYG
It would be amazing to have support for minimal rich text formatting on a text area field.
What about something like Slate?
I would need it as HTML and Markdown for different contexts.
Definitely on our radar! Thanks for opening a ticket - I've been meaning to.
Implementation wise I imagine we might use something like Slate, but not entirely sure.
One thing I'm not sure of is whether a WYSIWYG RTE is the same field type as a Markdown editor.
I am planning on adding this feature as a custom prop. My plan is to use Blocksuite, which is WYSIWYG with MD export as well. :)
@chrisvxd I've been using Lexical within a custom field and like the approach so far. Lexical has less out of the box, but is one of the most extensible/customizable libs for building a rich text editor. Could be worth looking into. The lexical plugin model would allow some very cool integration with the puck editor at large, like dropping content directly into the rich text field to be rendered along with text content.
How does Lexical compare to Tiptap? @kadencartwright
How does Lexical compare to Tiptap? @kadencartwright
From my research they cover a similar use case, as both are more about providing the tools to build a rich text editor rather than giving you something out of the box.
I haven’t used tip tap personally but was turned off by the freemium model as it generally makes projects significantly more susceptible to enshittification in my experience
The thing is that right now we have multiple options and I think that it either needs proper research (to include it in the Puck core) or we should implement it as a plugin.
What I see are 3 main competitors:
- Tiptap
- Lexical
- Blocksuite
If anyone is interested, it's possible to implement this using a custom field type. I'd be curious to see how the different options work.
When I raised this issue, I thought Puck would have a single supported RTE that would be provided as a part of the out-of-box experience. This could (and probably should) live outside the core package but be available for the demo and next-recipe packages. For a project I'm working on, we need CKEditor, so I'm working on this as a custom field. I think CKEditor is far too hefty for many generic cases, but I'll share it an example when it's working.
@chrisvxd You wouldn't, by any chance, have an example of this in a public repo somewhere? Would love to see it in action. I'm wondering if the sidebar offers enough space for this - or whether it would mean having to custom-implement the interface to make space for the editor.
@xaviemirmon It's been a while - so just wanted to check if you ever implemented this and could link it?
@JD-Robbs I do not have a public RTE field, but there is an inline RTE component listed in awesome-puck (see related #150)
I'll update in the next few days. If you've got any questions about implementing your own, feel free to hit me up.
I'll update in the next few days. If you've got any questions about implementing your own, feel free to hit me up.
Thanks for your quick reply. I used the response in this thread as a base and built my own rich-text-editor using tiptap.
Rich text menu with editor is on the right sidebar and I only render the content in the middle viewport just like the example Text block. If we could improve the library in a way that it provides WYSIWYG built-in, it would be amazing.
Just a feedback: I think that the number of examples should be increased. For each platform (remix, react, vue etc.) there should be a subfolder that provides one command setup.
Any updates on the rich-text support?
Hey @GnussonNet!
We are currently planning to release inline text editing in version 0.20. This update will also introduce a registerOverlayPortal function, which allows you to remove the drag overlay from specific elements in your components so they remain interactive in the canvas. This would make it easy to implement your own version with tools like TipTap.
For now, you can create your own rich text editing field as a custom field. The only limitation with this approach is that you would need to handle rich text editing directly in the sidebar, not in the canvas itself.
For this issue, we're going to need to add a field config which allows for extension/modification of the existing editor config. There are two different instances of the menus (Inline and Field panel) which benefit from having separate configurations. There are two separate usecases that I feel the RTE should solve.
- Adding or removing included buttons for
menuandinlineMenu - Adding extensions and extra buttons for TipTap.
I also have grouping to allow to users to group actions.
Current implementation
My current implementation makes the overriding of the default config possible but is too cumbersome:
const myConfig = {
menu: {
...defaultRichTextConfig.menu,
text: defaultRichTextConfig.menu.text?.filter(
(item) => item.title !== "Bold"
),
},
inlineMenu: {
text: defaultRichTextConfig.inlineMenu.text?.filter(
(item) => item.title !== "Bold"
),
headings: defaultRichTextConfig.menu.headings,
},
};
Proposal 1
Extend the existing implementation with the addition of menu and inlineMenu field props. For grouping I am suggesting we flatten default config and map against the new props defining spacers manually with something like a null value. This way, there it's simple adjust the two menus e.g.:
const RichTextInner: ComponentConfig<RichTextProps> = {
fields: {
richtext: {
type: "richtext",
contentEditable: true,
menu: [
"Bold",
"Italic",
null, // spacer
"AlignLeft"
"AlignRight"
...
]
inlineMenu: [
"Bold",
"Italic"
...
]
},
},
render: ({ richtext }) => {
return <Section>{richtext}</Section>;
},
defaultProps: {
richtext:
"<p style='text-align: center;'><strong>✍️ <s>Plain-text</s> Rich Text Editor! 🎉</strong></p>",
},
};
But then extra config and extensions can still be added to allow for extending of the editor.
import MyExtension from "./lib"
richtext: {
type: "richtext",
contentEditable: true,
menu: [
"Bold",
"Italic",
null, // spacer
"AlignLeft"
"AlignRight"
"MyAction"
...
]
inlineMenu: [
"Bold",
"Italic"
...
],
config: [
{
title: "MyAction",
icon: <Icon size={20} />,
action: (e: Editor) => e.chain().setMyAction("something").run(),
state: (e: Editor) => e.isActive({ myAction( "something" }),
},
...// custom config added here
],
extensions: [
MyExtension
]
},
The downside to this approach is that would need to render everything as a button and I don't know how I feel about the spacers as a null value
Proposal 2
Have the same props as per proposal one but add something similar to component categories in config.
import MyExtension from "./lib"
richtext: {
type: "richtext",
contentEditable: true,
menu: {
alignment: {
items: ["AlignLeft", "AlignCenter", "AlignRight", "Justify"],
},
typography: {
type: "dropdown" // default button, button || dropdown
items: ["p", "h2", "h3", ...],
},
other: {
items: ["InlineCode", "BulletList", ...],
},
},
inlineMenu: {
alignment: {
items: ["AlignLeft", "AlignCenter", "AlignRight", "Justify"],
},
...
}
config: [
{
title: "MyAction",
icon: <Icon size={20} />,
action: (e: Editor) => e.chain().setMyAction("something").run(),
state: (e: Editor) => e.isActive({ myAction( "something" }),
},
...// custom config added here
],
extensions: [
MyExtension
]
},
Proposal 3
As above but allow for full manipulation of the config and extensions as a resolve function.
import MyExtension from "./lib"
richtext: {
type: "richtext",
contentEditable: true,
menu: {
alignment: {
items: ["AlignLeft", "AlignCenter", "AlignRight", "Justify"],
},
typography: {
type: "dropdown" // default button, button || dropdown
items: ["p", "h2", "h3", ...],
},
other: {
items: ["InlineCode", "BulletList", ...],
},
},
inlineMenu: {
alignment: {
items: ["AlignLeft", "AlignCenter", "AlignRight", "Justify"],
},
...
}
resolveConfig: (config) => [
...config,
{
title: "MyAction",
icon: <Icon size={20} />,
action: (e: Editor) => e.chain().setMyAction("something").run(),
state: (e: Editor) => e.isActive({ myAction( "something" }),
},
...// custom config added here
],
resolveExtensions: (extensions) => [
extensions.StarterKit,
...
MyExtension
]
},
I'm personally leaning to option 2 but there may be some edge cases where users may want to unset existing config or extensions.
Here's a 4th proposal that might work, forked from Proposal 2. This makes a few tweaks:
- Use
inline | selectfortypefield (buttonfeels incorrect, as it's a group of buttons) - Replace
configwithactions(more explicit) - Provide full Tiptap API to action config via
renderfunction
Proposal 4
import MyExtension from "./lib"
richtext: {
type: "richtext",
contentEditable: true,
menu: {
alignment: {
items: ["AlignLeft", "AlignCenter", "AlignRight", "Justify"],
},
typography: {
type: "select" // default inline, inline || select
items: ["p", "h2", "h3", ...],
},
other: {
items: ["InlineCode", "BulletList", ...],
},
},
inlineMenu: {
alignment: {
items: ["AlignLeft", "AlignCenter", "AlignRight", "Justify"],
},
...
}
actions: {
MyAction: {
render: ({ editor }) => {
return <IconButton onClick={() => editor.chain().focus().toggleBold().run()}>
<Icon size={20} />
</IconButton>
}
}
},
extensions: [
MyExtension
]
},
This looks good with the exception of the MyAction is structured. For Tiptap, every action has 3 primary commands to it.
| Phase | What you check/do | Method(s) | Purpose |
|---|---|---|---|
| State | Is this active right now? | editor.isActive(name, attrs?) | Used to reflect UI state (e.g. highlight toolbar buttons) |
| Allowed | Can this action be performed? | editor.can(). |
Used to enable/disable UI controls safely |
| Execute | Perform the action | editor.chain(). |
Actually mutate the document |
Also, the render would override what's being set in the menu config and I can't see how this would allow for the select type.
How about something like this?:
actions: {
MyAction: {
state: ({ editor }) => editor.isActive(name, attrs?)
allowed: ({ editor }) => editor.can().<cmd>().run()
can: ({ editor }) => editor.chain().<cmd>().run()
icon: <Icon size={20} />
}
},
After discussing here's what I think we've landed on on:
Proposal 5
import MyExtension from "./lib"
richtext: {
type: "richtext",
contentEditable: true,
menu: {
alignment: ["AlignLeft", "AlignCenter", "AlignRight", "Justify"],
typography: ["TextSelect"],
other: ["InlineCode", "BulletList", ...]
},
inlineMenu: {
alignment: ["AlignLeft", "AlignCenter", "AlignRight", "Justify"],
typography: ["TextSelect"],
...
},
textSelectOptions: ["p", "h1", "h2", ... ]
controls: {
MyAction: {
render: ({ editor }) => {
return <IconButton onClick={() => editor.chain().focus().toggleBold().run()}>
<Icon size={20} />
</IconButton>
}
}
},
extensions: [
MyExtension
]
},
Hi is there any update about this? Puck 0.2 is already released and i could not find any info about rich editor.
I tried implementing it by myself with tiptap, but puckeditor does not allow me to use space button..
Hey @somegooser! You can check the progress here #1341
Closed by #1341! Available now in 0.21.0-canary.c78dc826 and will be released in 0.21.0.