Skip to content

Allows RichTextEditor to have custom buttons#629

Merged
AnnMarieW merged 20 commits intosnehilvj:masterfrom
BSd3v:rte-custom-buttons
Aug 20, 2025
Merged

Allows RichTextEditor to have custom buttons#629
AnnMarieW merged 20 commits intosnehilvj:masterfrom
BSd3v:rte-custom-buttons

Conversation

@BSd3v
Copy link
Copy Markdown
Contributor

@BSd3v BSd3v commented Aug 14, 2025

closes #618

example:

app.py

import dash_mantine_components as dmc
from dash import Dash
from dash_iconify import DashIconify

app=Dash()

content = """<h2 style="text-align: center;">Welcome to Mantine rich text editor</h2><p><code>RichTextEditor</code> component focuses on usability and is designed to be as simple as possible to bring a familiar editing experience to regular users. <code>RichTextEditor</code> is based on <a href="https://tiptap.dev/" rel="noopener noreferrer" target="_blank">Tiptap.dev</a> and supports all of its features:</p><ul><li>General text formatting: <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <s>strike-through</s> </li><li>Headings (h1-h6)</li><li>Sub and super scripts (<sup>&lt;sup /&gt;</sup> and <sub>&lt;sub /&gt;</sub> tags)</li><li>Ordered and bullet lists</li><li>Text align&nbsp;</li><li>And all <a href="https://tiptap.dev/extensions" target="_blank" rel="noopener noreferrer">other extensions</a></li></ul>"""



toolbar = {
    "sticky": True,
    "controlsGroups": [
        [
            "Bold",
            "Italic",
            {"Color": {"color": "red"}},
            {
                "CustomControl": {
                    "ariaLabel": "Custom Button",
                    "title": "Custom Button",
                   # "children": DashIconify(icon="mdi:star", width=20, height=20),
                    "children": "⭐",
                    "function": "insertStar",
                },
            },
        ],
        ["H1", "H2", "H3", "H4"],
    ],
}
component = dmc.RichTextEditor(
    html=content,
    toolbar=toolbar
)


app.layout = dmc.MantineProvider(
    component
)

if __name__ == "__main__":
    app.run(debug=True)

js

const dmcFuncs = window.dashMantineFunctions = window.dashMantineFunctions || {};

dmcFuncs.insertStar = (_,__,{editor}) => {
    if (!editor) {
        return;
    }
    editor?.commands.insertContent('⭐')
}

dmcFuncs.insertHeart = (_,__,{editor}) => {
    if (!editor) {
        return;
    }
    editor?.commands.insertContent('❤️')
}

dmcFuncs.insertTongueOut = (_,__,{editor}) => {
    if (!editor) {
        return;
    }
    editor?.commands.insertContent('😛')
}

Also fixed an issue where controlsGroups wouldnt allow to be empty or undefined.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Aug 14, 2025

Test Environment for snehilvj/dash-mantine-components-629
Updated on: 2025-08-15 22:42:26 UTC

@AnnMarieW
Copy link
Copy Markdown
Collaborator

This looks great! Thanks for the PR 🏆

I'd like to see the "custom" key be capitalized so it's consistent with the other syntax in the tooltip prop.

Comment thread src/ts/components/extensions/richtexteditor/RichTextEditor.tsx Outdated
Comment thread src/ts/components/extensions/richtexteditor/RichTextEditor.tsx Outdated
Comment thread src/ts/components/extensions/richtexteditor/fragments/RichTextEditor.tsx Outdated
Comment thread src/ts/components/extensions/richtexteditor/fragments/RichTextEditor.tsx Outdated
const controlName = Object.keys(ctl)[0];
const options = ctl[controlName];

if (controlName !== 'CustomButton') {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (controlName !== 'CustomButton') {
if (controlName !== 'CustomControl') {

Comment thread src/ts/components/extensions/richtexteditor/fragments/RichTextEditor.tsx Outdated
BSd3v and others added 4 commits August 15, 2025 16:59
Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
…Editor.tsx

Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
…Editor.tsx

Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
…Editor.tsx

Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
@AnnMarieW
Copy link
Copy Markdown
Collaborator

Here is a sample app for custom Insert Table, Add Column and Delete Column as requested in #618

See it live on PyCafe:
https://py.cafe/amward/dash-mantine-rich-text-editor

rte_custom_controls

import dash_mantine_components as dmc
from dash import Dash
from dash_iconify import DashIconify

app=Dash()

content = """<h2 style="text-align: center;">RichTextEditor Custom Controls Demo</h2>"""

toolbar = {
    "sticky": True,
    "controlsGroups": [
        [

            {
                "CustomControl": {
                    "ariaLabel": "Insert Table",
                    "title": "Insert Table",
                    "children": [DashIconify(icon="mdi:table-plus", width=20, height=20)],
                    "function": "insertTable",
                },
            },
            {
                "CustomControl": {
                    "ariaLabel": "Add Column Before",
                    "title": "Add Column Before",
                    "children": [DashIconify(icon="mdi:table-column-plus-before", width=20, height=20)],
                    "function": "addColumnBefore",
                },
            },
            {
                "CustomControl": {
                    "ariaLabel": "Delete Column",
                    "title": "Delete Column",
                    "children": [DashIconify(icon="mdi:table-column-remove", width=20, height=20)],
                    "function": "deleteColumn",
                },
            },
        ],
        [
            "Bold",
            "Italic",
            "Underline",
        ],
    ],
}

app.layout = dmc.MantineProvider(
    dmc.RichTextEditor(
        html=content,
        toolbar=toolbar
    )
)

if __name__ == "__main__":
    app.run(debug=True)

.js

var dmcfuncs = window.dashMantineFunctions = window.dashMantineFunctions || {};

dmcfuncs.insertTable = (_,__,{editor}) => {
    if (!editor) {
        return;
    }
    editor?.chain().focus().insertTable({ rows: 5, cols: 3, withHeaderRow: true }).run()
}


dmcfuncs.addColumnBefore = (_,__,{editor}) => {
    if (!editor) {
        return;
    }
    editor?.chain().focus().addColumnBefore().run()
}


dmcfuncs.deleteColumn= (_,__,{editor}) => {
    if (!editor) {
        return;
    }
    editor?.chain().focus().deleteColumn().run()
}

.css

table, th, td {
        border: 1px solid var(--table-border-color);
    }


@AnnMarieW
Copy link
Copy Markdown
Collaborator

The way we have it now, it's possible to place the custom control anywhere in the toolbar but it must be in a controlsGroups.
I think that's OK since it's unlikely that people will have only one control, and it's by itself (like this minimal example for the docs).

component = dmc.RichTextEditor(
    html= '<div>Click control to insert star emoji</div>',
    toolbar = {
        "controlsGroups": [
            [
                {
                    "CustomControl": {
                        "aria-label": "Custom Button",
                        "title": "Custom Button",
                        "children": DashIconify(icon="mdi:star", width=20, height=20),
                        "function": "insertStar",
                    },
                },
            ],
        ],
    },
)

>
{newRenderDashComponent(children, i, [
...componentPath,
'custom',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the custom prop for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to make the path right for Dash, it needs to be updated too.

@AnnMarieW
Copy link
Copy Markdown
Collaborator

AnnMarieW commented Aug 16, 2025

is there a use-case for the first two props in the function? (_,__,{editor}) Not sure what they do.

dmcFuncs.insertStar = (_,__,{editor}) => {
    if (!editor) {
        return;
    }
    editor?.commands.insertContent('⭐')
}

Update

OK, I figured it out.... The first two props are event( from the onClick function) and options from the resolveProp feature. To make this more obvious, we should only pass the onClick prop to the CustomControl component, and include in the docs an example of how to pass the options to the function.

fragments/RichTextEditor.tsx


const CustomControl = (props) => {
    const { i, componentPath, editor, onClick, children, ...others } = props;
    return (
        <MantineRichTextEditor.Control
            onClick={resolveProp(onClick, { editor })}   // here's the change - just pass onClick prop
            {...others}
        >
            {newRenderDashComponent(children, i, [
                ...componentPath,
                'custom',
                'children',
            ])}
        </MantineRichTextEditor.Control>
    );
};

Here's a use-case, where you can make a re-usable function to insert content:

.js file:

dmcfuncs.insertContent = (e, options, {editor}) => {    
     if (!editor) {
        return;
    }
    editor?.commands.insertContent(options)

}
import dash_mantine_components as dmc
from dash import Dash
from dash_iconify import DashIconify

app=Dash()

content = """<h2 style="text-align: center;">RichTextEditor Custom Controls Demo</h2>"""

toolbar = {
    "sticky": True,
    "controlsGroups": [
        [

            {
                "CustomControl": {
                    "ariaLabel": "Insert Star",
                    "title": "Insert Star",
                    "children": [DashIconify(icon="mdi:star", width=20, height=20)],
                    "onClick": {"function": "insertContent", "options": "⭐"},
                },
            },
            {
                "CustomControl": {
                    "ariaLabel": "Insert Heart",
                    "title": "Insert Heart",
                    "children": [DashIconify(icon="mdi:heart", width=20, height=20)],
                    "onClick": {"function": "insertContent", "options": "❤️"},
                },
            },

        ],

    ],
}

app.layout = dmc.MantineProvider(
    dmc.RichTextEditor(
        html=content,
        toolbar=toolbar
    )
)

if __name__ == "__main__":
    app.run(debug=True)

@AnnMarieW
Copy link
Copy Markdown
Collaborator

Updated examples in the dmc-docs PR snehilvj/dmc-docs#239

@AnnMarieW AnnMarieW requested a review from alexcjohnson August 18, 2025 17:22
const { i, componentPath, editor, onClick, children, ...others } = props;
return (
<MantineRichTextEditor.Control
onClick={resolveProp(onClick, { editor })}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there precedent here for this argument pattern, ie using resolveProp directly? Having two typically-ignored arguments up front and then a third that you always have to destructure is awkward. You could wrap resolveProp here in order to change this to (editor, event, options), and even put the editor existence guard into the wrapper if that's always going to be part of any real handler function.

If I'm understanding it correctly that would be something like:

onClick={(event) => {
    if (editor) {
         resolveProp(onClick)(editor, event);
    }
}}

That way you'd have much simpler functions most of the time:

dmcfuncs.insertContent = (editor, _, options) => {
    editor?.commands.insertContent(options)
}

That still leaves event in the middle as an often-ignored argument. I guess you could also put it in context:

onClick={(event) => {
    if (editor) {
         resolveProp(onClick, { event })(editor);
    }
}}

then nearly everyone would omit it entirely:

dmcfuncs.insertContent = (editor, options) => {
    editor?.commands.insertContent(options)
}

but if you did want it (to detect modifier keys or something?) you could use (editor, options, {event})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like having the attributes that are supplied by the function first, then options (passed from the dash app) last, so it's consistent with with all the other functions as props in dmc.

@BSd3v any thoughts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think that works. I was passing is as an object in the event that we wanted to expand it at some point to maybe pass other things from the RichTextEditor instance.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexcjohnson

Bryan also suggested passing both editor and event in an object. The latest commit makes it so can now do this:

dmcfuncs.insertContent = ({editor}, options) => {
    editor?.commands.insertContent(options)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me!

Copy link
Copy Markdown
Collaborator

@alexcjohnson alexcjohnson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💃

@AnnMarieW AnnMarieW merged commit 998bd92 into snehilvj:master Aug 20, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Rich Text Editor - Add Table + Add/Remove Columns

3 participants