Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Quill RichTextInput With TipTap RichTextInput #7153

Merged
merged 21 commits into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2386,6 +2386,81 @@ test('should use counter', () => {
})
```

## `richt-text-input` Package Has Changed

Our old `<RichTextInput>` was based on [Quill](https://quilljs.com/) but:
- it wasn't accessible (button without labels, etc.)
- it wasn't translatable (labels in Quill are in the CSS)
- it wasn't using MUI components for its UI and looked off

The new `<RichTextInput>` uses [TipTap](https://github.com/ueberdosis/tiptap), a UI less library to build rich text editors. It gives us the freedom to implement the UI how we want with MUI components. That solves all the above issues.

If you used the `<RichTextInput>` without passing Quill options such as custom toolbars, you have nothing to do.

If you customized the available buttons with the `toolbar` props, you can now use the components we provide:

```diff
const MyRichTextInput = (props) => (
<RichTextInput
{...props}
- toolbar={[ ['bold', 'italic', 'underline', 'link'] ]}
+ toolbar={
<RichTextInputToolbar>
<FormatButtons size={size} />
<LinkButtons size={size} />
</RichTextInputToolbar>
}
/>
)
```

If you customized the Quill instance to add custom handlers, you'll have to use [TipTap](https://github.com/ueberdosis/tiptap) primitives.

```diff
import {
RichTextInput,
+ DefaultEditorOptions,
+ RichTextInputToolbar,
+ RichTextInputLevelSelect,
+ FormatButtons,
+ AlignmentButtons,
+ ListButtons,
+ LinkButtons,
+ QuoteButtons,
+ ClearButtons,
} from 'ra-input-rich-text';

-const configureQuill = quill => quill.getModule('toolbar').addHandler('insertSmile', function (value) {
- const { index, length } = this.quill.getSelection();
- this.quill..insertText(index + length, ':-)', 'api');
-});

const MyRichTextInput = (props) => (
<RichTextInput
{...props}
- configureQuill={configureQuill}
+ toolbar={
+ <RichTextInputToolbar>
+ <RichTextInputLevelSelect size={size} />
+ <FormatButtons size={size} />
+ <AlignmentButtons {size} />
+ <ListButtons size={size} />
+ <LinkButtons size={size} />
+ <QuoteButtons size={size} />
+ <ClearButtons size={size} />
+ <ToggleButton
+ aria-label="Add a smile"
+ title="Add a smile"
+ onClick={() => editor.insertContent(':-)')}
+ >
+ <Remove fontSize="inherit" />
+ </ToggleButton>
+ </RichTextInputToolbar>
}
/>
}
```

# Upgrade to 3.0

We took advantage of the major release to fix all the problems in react-admin that required a breaking change. As a consequence, you'll need to do many small changes in the code of existing react-admin v2 applications. Follow this step-by-step guide to upgrade to react-admin v3.
Expand Down
22 changes: 12 additions & 10 deletions cypress/integration/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,19 +299,22 @@ describe('Create Page', () => {
);
});

// FIXME Skipped as we are going to replace the RichTextInput with the tip tap version
it.skip('should not show rich text input error message when field is untouched', () => {
cy.get(CreatePage.elements.richTextInputError).should('not.have.value');
it('should not show rich text input error message when field is untouched', () => {
cy.get(CreatePage.elements.richTextInputError).should('not.exist');
});

// FIXME Skipped as we are going to replace the RichTextInput with the tip tap version
it.skip('should show rich text input error message when form is submitted', () => {
it('should show rich text input error message when form is submitted', () => {
const values = [
{
type: 'input',
name: 'title',
value: 'Test title',
},
{
type: 'textarea',
name: 'teaser',
value: 'Test teaser',
},
];
CreatePage.setValues(values);
CreatePage.submit(false);
Expand All @@ -320,8 +323,7 @@ describe('Create Page', () => {
.contains('Required');
});

// FIXME Skipped as we are going to replace the RichTextInput with the tip tap version
it.skip('should not show rich text input error message when form is submitted and input is filled with text', () => {
it('should not show rich text input error message when form is submitted and input is filled with text', () => {
const values = [
{
type: 'input',
Expand All @@ -338,9 +340,9 @@ describe('Create Page', () => {
// Quill take a little time to boot and Cypress is too fast which can leads to unstable tests
WiXSL marked this conversation as resolved.
Show resolved Hide resolved
// so we wait a bit before interacting with the rich-text-input
cy.wait(250);
cy.get(CreatePage.elements.input('body', 'rich-text-input')).type(
'text'
);
cy.get(CreatePage.elements.input('body', 'rich-text-input'))
.type('text')
.blur();
cy.get(CreatePage.elements.richTextInputError).should('not.exist');
});

Expand Down
2 changes: 1 addition & 1 deletion cypress/integration/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ describe('Edit Page', () => {
cy.get(CreatePostPage.elements.input('body', 'rich-text-input')).should(
el =>
// When the Quill editor is empty, it add the "ql-blank" CSS class
djhi marked this conversation as resolved.
Show resolved Hide resolved
expect(el).to.have.class('ql-blank')
expect(el.text()).to.equal('')
);
});

Expand Down
4 changes: 2 additions & 2 deletions cypress/support/CreatePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default url => ({
body: 'body',
input: (name, type = 'input') => {
if (type === 'rich-text-input') {
return `.ra-input-${name} .ql-editor`;
return `.ra-input-${name} .ProseMirror`;
}
return `.create-page ${type}[name='${name}']`;
},
Expand All @@ -18,7 +18,7 @@ export default url => ({
".create-page form div[role='toolbar'] button[type='button']:nth-child(3)",
submitCommentable:
".create-page form div[role='toolbar'] button[type='button']:last-child",
descInput: '.ql-editor',
descInput: '.ProseMirror',
tab: index => `.form-tab:nth-of-type(${index})`,
title: '#react-admin-title',
userMenu: 'button[aria-label="Profile"]',
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/EditPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default url => ({
removeBacklinkButton: '[aria-label="Remove"]',
input: (name, type = 'input') => {
if (type === 'rich-text-input') {
return `.ra-input-${name} .ql-editor`;
return `.ra-input-${name} .ProseMirror`;
}
if (type === 'checkbox-group-input') {
return `.ra-input-${name} label`;
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/ListPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,6 @@ export default url => ({
},

toggleColumnSort(name) {
cy.get(this.elements.sortBy(name)).click();
cy.get(this.elements.sortBy(name)).click().blur();
},
});
161 changes: 112 additions & 49 deletions docs/RichTextInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,79 +5,142 @@ title: "The RichTextInput Component"

# `<RichTextInput>`

`<RichTextInput>` is the ideal component if you want to allow your users to edit some HTML contents. It is powered by [Quill](https://quilljs.com/).
`<RichTextInput>` is the ideal component if you want to allow your users to edit some HTML contents. It is powered by [TipTap](https://www.tiptap.dev/).

![RichTextInput](./img/rich-text-input.gif)

**Note**: Due to its size, `<RichTextInput>` is not bundled by default with react-admin. You must install it first, using npm:

```sh
npm install ra-input-rich-text
# or
yarn add ra-input-rich-text
```

Then use it as a normal input component:
Use it as you would any react-admin inputs:

```jsx
import RichTextInput from 'ra-input-rich-text';

<RichTextInput source="body" />
import { Edit, SimpleForm, TextInput } from 'react-admin';
import { RichTextInput } from 'ra-input-rich-text';

export const PostEdit = (props) => (
<Edit {...props}>
<SimpleForm>
<TextInput source="title" />
<RichTextInput source="body" />
</SimpleForm>
</Edit>
);
```

You can customize the rich text editor toolbar using the `toolbar` attribute, as described on the [Quill official toolbar documentation](https://quilljs.com/docs/modules/toolbar/).
## Customizing the Toolbar

```jsx
<RichTextInput source="body" toolbar={[ ['bold', 'italic', 'underline', 'link'] ]} />
```
The `<RichTextInput>` component has a `toolar` prop that accepts a `ReactNode`.

If you need to add Quill `modules` or `themes`, you can do so by passing them in the `options` prop.
You can leverage this to change the buttons [size](#api):

{% raw %}
```jsx
<RichTextInput
source="body"
options={{
modules: {
history: { // History module
delay: 2000,
maxStack: 500,
userOnly: true
}
},
theme: "snow"
}}
/>
import { Edit, SimpleForm, TextInput } from 'react-admin';
import { RichTextInput, RichTextInputToolbar } from 'ra-input-rich-text';

export const PostEdit = (props) => (
<Edit {...props}>
<SimpleForm>
<TextInput source="title" />
<RichTextInput source="body" toolbar={<RichTextInputToolbar size="large" />} />
</SimpleForm>
</Edit>
);
```
{% endraw %}

If you need more customization, you can access the quill object through the `configureQuill` callback that will be called just after its initialization.
Or to remove some prebuilt components like the `<AlignmentButtons>`:

```jsx
const configureQuill = quill => quill.getModule('toolbar').addHandler('bold', function (value) {
this.quill.format('bold', value)
});

// ...

<RichTextInput source="text" configureQuill={configureQuill}/>
import {
RichTextInput,
RichTextInputToolbar,
RichTextInputLevelSelect,
FormatButtons,
ListButtons,
LinkButtons,
QuoteButtons,
ClearButtons,
} from 'ra-input-rich-text';

const MyRichTextInput = ({ size, ...props }) => (
<RichTextInput
toolbar={
<RichTextInputToolbar>
<RichTextInputLevelSelect size={size} />
<FormatButtons size={size} />
<ListButtons size={size} />
<LinkButtons size={size} />
<QuoteButtons size={size} />
<ClearButtons size={size} />
</RichTextInputToolbar>
}
label="Body"
source="body"
{...props}
/>
);
```

`<RichTextInput>` also accepts the [common input props](./Inputs.md#common-input-props).
## Customizing the editor

**Tip**: When used inside a material-ui `<Card>` (e.g. in the default `<Edit>` view), `<RichTextInput>` displays link tooltip as cut off when the user wants to add a hyperlink to a word located on the left side of the input. This is due to an incompatibility between material-ui's `<Card>` component and Quill's positioning algorithm for the link tooltip.
You might want to add more Tiptap extensions. The `<RichTextInput>` component accepts an `editorOptions` prop which is the [object passed to Tiptap Editor](https://www.tiptap.dev/guide/configuration).

To fix this problem, you should override the default card style, as follows:
If you just want to **add** extensions, don't forget to include those needed by default for our implementation. Here's an example to add the [HorizontalRule node](https://www.tiptap.dev/api/nodes/horizontal-rule):

```diff
import { Edit, SimpleForm, TextInput } from 'react-admin';
+import { withStyles } from '@mui/material/styles';

-const PostEdit = props => (
+const PostEdit = withStyles({ card: { overflow: 'initial' } })(() => (
<Edit>
<SimpleForm>
// ...
</SimpleForm>
</Edit>
-);
+));
```jsx
import {
DefaultEditorOptions,
RichTextInput,
RichTextInputToolbar,
RichTextInputLevelSelect,
FormatButtons,
AlignmentButtons,
ListButtons,
LinkButtons,
QuoteButtons,
ClearButtons,
} from 'ra-input-rich-text';
import HorizontalRule from '@tiptap/extension-horizontal-rule';
import Remove from '@material-ui/icons/Remove';

const MyRichTextInput = ({ size, ...props }) => (
<RichTextInput
editorOptions={MyEditorOptions}
toolbar={
<RichTextInputToolbar>
<RichTextInputLevelSelect size={size} />
<FormatButtons size={size} />
<AlignmentButtons {size} />
<ListButtons size={size} />
<LinkButtons size={size} />
<QuoteButtons size={size} />
<ClearButtons size={size} />
<ToggleButton
aria-label="Add an horizontal rule"
title="Add an horizontal rule"
onClick={() => editor.chain().focus().setHorizontalRule().run()}
selected={editor && editor.isActive('horizontalRule')}
>
<Remove fontSize="inherit" />
</ToggleButton>
</RichTextInputToolbar>
}
label="Body"
source="body"
{...props}
/>
);

export const MyEditorOptions = {
...DefaultEditorOptions,
extensions: [
...DefaultEditorOptions.extensions,
HorizontalRule,
],
};
```
Binary file modified docs/img/rich-text-input.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion examples/demo/src/ra-input-rich-text.d.ts

This file was deleted.

Loading