diff --git a/UPGRADE.md b/UPGRADE.md
index 84d2749ccfc..b2e510140aa 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -2386,6 +2386,81 @@ test('should use counter', () => {
})
```
+## `richt-text-input` Package Has Changed
+
+Our old `` 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 `` 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 `` 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) => (
+
+
+
+
+ }
+ />
+)
+```
+
+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) => (
+
++
++
++
++
++
++
++
++ editor.insertContent(':-)')}
++ >
++
++
++
+ }
+ />
+}
+```
+
# 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.
diff --git a/cypress/integration/create.js b/cypress/integration/create.js
index d645620aa58..7b0521ae23d 100644
--- a/cypress/integration/create.js
+++ b/cypress/integration/create.js
@@ -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);
@@ -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',
@@ -335,12 +337,9 @@ describe('Create Page', () => {
.should('exist')
.contains('Required');
- // Quill take a little time to boot and Cypress is too fast which can leads to unstable tests
- // 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');
});
diff --git a/cypress/integration/edit.js b/cypress/integration/edit.js
index 3e5e5c9aec2..2d9e2e3709f 100644
--- a/cypress/integration/edit.js
+++ b/cypress/integration/edit.js
@@ -147,11 +147,9 @@ describe('Edit Page', () => {
// it didn't initiate.
cy.contains('Create post').click();
- cy.get(CreatePostPage.elements.input('body', 'rich-text-input')).should(
- el =>
- // When the Quill editor is empty, it add the "ql-blank" CSS class
- expect(el).to.have.class('ql-blank')
- );
+ cy.get(
+ CreatePostPage.elements.input('body', 'rich-text-input')
+ ).should(el => expect(el.text()).to.equal(''));
});
it('should allow to select an item from the AutocompleteInput without showing the choices again after', () => {
diff --git a/cypress/support/CreatePage.js b/cypress/support/CreatePage.js
index f21fd5af8cc..ac6ecda6ac0 100644
--- a/cypress/support/CreatePage.js
+++ b/cypress/support/CreatePage.js
@@ -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}']`;
},
@@ -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"]',
diff --git a/cypress/support/EditPage.js b/cypress/support/EditPage.js
index 099d90c2e7d..b4e6c21ee57 100644
--- a/cypress/support/EditPage.js
+++ b/cypress/support/EditPage.js
@@ -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`;
diff --git a/cypress/support/ListPage.js b/cypress/support/ListPage.js
index d0971446218..2b389efe72d 100644
--- a/cypress/support/ListPage.js
+++ b/cypress/support/ListPage.js
@@ -125,6 +125,6 @@ export default url => ({
},
toggleColumnSort(name) {
- cy.get(this.elements.sortBy(name)).click();
+ cy.get(this.elements.sortBy(name)).click().blur();
},
});
diff --git a/docs/RichTextInput.md b/docs/RichTextInput.md
index d46601bab40..7389e2818c1 100644
--- a/docs/RichTextInput.md
+++ b/docs/RichTextInput.md
@@ -5,7 +5,7 @@ title: "The RichTextInput Component"
# ``
-`` is the ideal component if you want to allow your users to edit some HTML contents. It is powered by [Quill](https://quilljs.com/).
+`` 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/).

@@ -13,71 +13,134 @@ title: "The RichTextInput Component"
```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';
-
-
+import { Edit, SimpleForm, TextInput } from 'react-admin';
+import { RichTextInput } from 'ra-input-rich-text';
+
+export const PostEdit = (props) => (
+
+
+
+
+
+
+);
```
-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
-
-```
+The `` 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
-
+import { Edit, SimpleForm, TextInput } from 'react-admin';
+import { RichTextInput, RichTextInputToolbar } from 'ra-input-rich-text';
+
+export const PostEdit = (props) => (
+
+
+
+ } />
+
+
+);
```
-{% 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 ``:
```jsx
-const configureQuill = quill => quill.getModule('toolbar').addHandler('bold', function (value) {
- this.quill.format('bold', value)
-});
-
-// ...
-
-
+import {
+ RichTextInput,
+ RichTextInputToolbar,
+ RichTextInputLevelSelect,
+ FormatButtons,
+ ListButtons,
+ LinkButtons,
+ QuoteButtons,
+ ClearButtons,
+} from 'ra-input-rich-text';
+
+const MyRichTextInput = ({ size, ...props }) => (
+
+
+
+
+
+
+
+
+ }
+ label="Body"
+ source="body"
+ {...props}
+ />
+);
```
-`` also accepts the [common input props](./Inputs.md#common-input-props).
+## Customizing the editor
-**Tip**: When used inside a material-ui `` (e.g. in the default `` view), `` 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 `` component and Quill's positioning algorithm for the link tooltip.
+You might want to add more Tiptap extensions. The `` 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' } })(() => (
-
-
- // ...
-
-
--);
-+));
+```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 }) => (
+
+
+
+
+
+
+
+
+ editor.chain().focus().setHorizontalRule().run()}
+ selected={editor && editor.isActive('horizontalRule')}
+ >
+
+
+
+ }
+ label="Body"
+ source="body"
+ {...props}
+ />
+);
+
+export const MyEditorOptions = {
+ ...DefaultEditorOptions,
+ extensions: [
+ ...DefaultEditorOptions.extensions,
+ HorizontalRule,
+ ],
+};
```
diff --git a/docs/img/rich-text-input.gif b/docs/img/rich-text-input.gif
index 789be3665d7..13d2a776eef 100644
Binary files a/docs/img/rich-text-input.gif and b/docs/img/rich-text-input.gif differ
diff --git a/examples/demo/src/ra-input-rich-text.d.ts b/examples/demo/src/ra-input-rich-text.d.ts
deleted file mode 100644
index e9040f114c8..00000000000
--- a/examples/demo/src/ra-input-rich-text.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module 'ra-input-rich-text';
diff --git a/examples/simple/src/posts/PostCreate.tsx b/examples/simple/src/posts/PostCreate.tsx
index 3e1e283245f..69379ab01aa 100644
--- a/examples/simple/src/posts/PostCreate.tsx
+++ b/examples/simple/src/posts/PostCreate.tsx
@@ -7,8 +7,13 @@ import {
BooleanInput,
Create,
DateInput,
+ FileField,
+ FileInput,
FormDataConsumer,
+ maxValue,
+ minValue,
NumberInput,
+ required,
ReferenceInput,
SaveButton,
SelectInput,
@@ -16,9 +21,6 @@ import {
SimpleFormIterator,
TextInput,
Toolbar,
- required,
- FileInput,
- FileField,
useNotify,
usePermissions,
useRedirect,
@@ -105,20 +107,6 @@ const PostCreate = () => {
}
defaultValues={defaultValues}
- validate={values => {
- const errors = {} as any;
- ['title', 'teaser'].forEach(field => {
- if (!values[field]) {
- errors[field] = 'Required field';
- }
- });
-
- if (values.average_note < 0 || values.average_note > 5) {
- errors.average_note = 'Should be between 0 and 5';
- }
-
- return errors;
- }}
>
{
>
-
-
-
+
+
+
-
+
{
source="body"
label=""
validate={required()}
- addLabel={false}
+ fullWidth
/>
diff --git a/package.json b/package.json
index d591fb086e0..71724d38a5d 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
"run-graphql-demo-watch": "concurrently \"yarn run watch\" \"yarn run run-graphql-demo\"",
"run-crm": "cd examples/crm && yarn start",
"build-crm": "cd examples/crm && yarn build",
- "storybook": "start-storybook -p 9009",
+ "storybook": "start-storybook -p 9010",
"build-storybook": "build-storybook --no-dll -c .storybook -o public --quiet"
},
"devDependencies": {
@@ -55,7 +55,6 @@
"lerna": "~4.0.0",
"lint-staged": "^8.1.7",
"lolex": "~2.3.2",
- "mutationobserver-shim": "^0.3.3",
"prettier": "~2.1.1",
"raf": "~3.4.1",
"react": "^17.0.0",
diff --git a/packages/ra-input-rich-text/README.md b/packages/ra-input-rich-text/README.md
index 65168d7dae5..6434bead888 100644
--- a/packages/ra-input-rich-text/README.md
+++ b/packages/ra-input-rich-text/README.md
@@ -1,79 +1,145 @@
-# `` for react-admin
+# ra-input-rich-text
-For editing HTML with [react-admin](https://github.com/marmelab/react-admin), use the `` component. It embarks [quill](https://quilljs.com/), a popular cross-platform Rich Text Editor.
-
-
+A rich text editor for [React Admin](http://marmelab.com/react-admin), based on [TipTap](https://www.tiptap.dev/)
## Installation
```sh
-npm install ra-input-rich-text --save-dev
+npm install ra-input-rich-text
+# or
+yarn add ra-input-rich-text
```
## Usage
+Use it as you would any react-admin inputs:
+
```jsx
-import * as React from "react";
-import {
- DateInput,
- Edit,
- EditButton,
- TextInput,
-} from 'react-admin';
-import RichTextInput from 'ra-input-rich-text';
-
-const PostTitle = ({ record }) => {
- return Post {record ? `"${record.title}"` : ''};
-};
+import { Edit, SimpleForm, TextInput } from 'react-admin';
+import { RichTextInput } from 'ra-input-rich-text';
export const PostEdit = (props) => (
- } {...props}>
-
-
-
-
-
-
+
+
+
+
+
+
);
```
-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
+
+The `` component has a `toolar` prop that accepts a `ReactNode`.
+
+You can leverage this to change the buttons [size](#api):
```jsx
-
+import { Edit, SimpleForm, TextInput } from 'react-admin';
+import { RichTextInput, RichTextInputToolbar } from 'ra-input-rich-text';
+
+export const PostEdit = (props) => (
+
+
+
+ } />
+
+
+);
```
-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 ``:
-```js
-const configureQuill = quill => quill.getModule('toolbar').addHandler('bold', function (value) {
- this.quill.format('bold', value)
-});
+```jsx
+import {
+ RichTextInput,
+ RichTextInputToolbar,
+ RichTextInputLevelSelect,
+ FormatButtons,
+ ListButtons,
+ LinkButtons,
+ QuoteButtons,
+ ClearButtons,
+} from 'ra-input-rich-text';
+
+const MyRichTextInput = ({ size, ...props }) => (
+
+
+
+
+
+
+
+
+ }
+ label="Body"
+ source="body"
+ {...props}
+ />
+);
+```
-// ...
+## Customizing the editor
-
-```
+You might want to add more Tiptap extensions. The `` component accepts an `editorOptions` prop which is the [object passed to Tiptap Editor](https://www.tiptap.dev/guide/configuration).
-**Tip**: When used inside a material-ui `` (e.g in the default `` view), `` 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 `` component and Quill's positioning algorithm for the link tooltip.
+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):
-To fix this problem, you should override the default card style, as follows:
+```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 }) => (
+
+
+
+
+
+
+
+
+ editor.chain().focus().setHorizontalRule().run()}
+ selected={editor && editor.isActive('horizontalRule')}
+ >
+
+
+
+ }
+ label="Body"
+ source="body"
+ {...props}
+ />
+);
-```diff
-import { Edit, SimpleForm, TextInput } from 'react-admin';
-+import { withStyles } from '@mui/material/styles';
-
--const PostEdit = props => (
-+const PostEdit = withStyles({ card: { overflow: 'initial' } })(props => (
-
-
- // ...
-
-
--);
-+));
+export const MyEditorOptions = {
+ ...DefaultEditorOptions,
+ extensions: [
+ ...DefaultEditorOptions.extensions,
+ HorizontalRule,
+ ],
+};
```
## License
-This library is licensed under the MIT License, and sponsored by [marmelab](https://marmelab.com).
+This data provider is licensed under the MIT License, and sponsored by [marmelab](https://marmelab.com).
diff --git a/packages/ra-input-rich-text/assets/demo.gif b/packages/ra-input-rich-text/assets/demo.gif
new file mode 100644
index 00000000000..04e025d9f27
Binary files /dev/null and b/packages/ra-input-rich-text/assets/demo.gif differ
diff --git a/packages/ra-input-rich-text/package.json b/packages/ra-input-rich-text/package.json
index 88b96fb4337..3374da097ea 100644
--- a/packages/ra-input-rich-text/package.json
+++ b/packages/ra-input-rich-text/package.json
@@ -2,62 +2,52 @@
"name": "ra-input-rich-text",
"version": "4.0.0-alpha.2",
"description": " component for react-admin, useful for editing HTML code in admin GUIs.",
- "main": "dist/index",
- "module": "dist/esm/index.js",
- "types": "dist/index.d.ts",
- "sideEffects": false,
+ "author": "Gildas Garcia",
+ "repository": "marmelab/react-admin",
+ "homepage": "https://github.com/marmelab/react-admin#readme",
+ "bugs": "https://github.com/marmelab/react-admin/issues",
+ "license": "MIT",
"files": [
- "LICENSE",
"*.md",
"dist",
"src"
],
- "repository": {
- "type": "git",
- "url": "git+https://github.com/marmelab/react-admin.git"
- },
- "keywords": [
- "reactjs",
- "react",
- "react-admin",
- "admin-on-rest",
- "rest",
- "richtext",
- "html",
- "wysiwyg",
- "editor"
- ],
- "author": "François Zaninotto",
- "license": "MIT",
- "bugs": {
- "url": "https://github.com/marmelab/react-admin/issues"
- },
- "homepage": "https://github.com/marmelab/react-admin#readme",
+ "main": "dist/index",
+ "module": "dist/esm/index.js",
+ "types": "dist/index.d.ts",
+ "sideEffects": false,
"scripts": {
"build": "tsup src/index.ts --silent --clean --format cjs,esm --minify --keep-names --metafile --sourcemap --dts --legacy-output",
"watch": "tsup src/index.ts --silent --clean --format cjs,esm --minify --keep-names --metafile --sourcemap --dts --legacy-output --watch"
},
+ "dependencies": {
+ "@tiptap/extension-link": "^2.0.0-beta.20",
+ "@tiptap/extension-placeholder": "^2.0.0-beta.30",
+ "@tiptap/extension-text-align": "^2.0.0-beta.23",
+ "@tiptap/extension-underline": "^2.0.0-beta.16",
+ "@tiptap/react": "^2.0.0-beta.63",
+ "@tiptap/starter-kit": "^2.0.0-beta.101",
+ "clsx": "^1.1.1"
+ },
"peerDependencies": {
+ "@mui/icons-material": "^5.0.1",
"@mui/material": "^5.0.2",
"ra-core": "^4.0.0-alpha.2",
"ra-ui-materialui": "^4.0.0-alpha.2",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0"
},
- "dependencies": {
- "@types/quill": "~1.3.0",
- "lodash": "~4.17.5",
- "prop-types": "^15.6.0",
- "quill": "~1.3.6"
- },
"devDependencies": {
+ "@mui/icons-material": "^5.0.1",
"@mui/material": "^5.0.2",
"@testing-library/react": "^11.2.3",
- "@types/prop-types": "^15.6.0",
- "npm-dts": "^1.3.10",
+ "data-generator-retail": "^4.0.0-alpha.2",
"ra-core": "^4.0.0-alpha.2",
+ "ra-data-fakerest": "^4.0.0-alpha.2",
"ra-ui-materialui": "^4.0.0-alpha.2",
- "rimraf": "^2.6.3",
+ "react": "^17.0.0",
+ "react-dom": "^17.0.0",
+ "react-hook-form": "^7.25.0",
"tsup": "^5.11.11"
}
}
diff --git a/packages/ra-input-rich-text/src/QuillBubbleStylesheet.ts b/packages/ra-input-rich-text/src/QuillBubbleStylesheet.ts
deleted file mode 100644
index 768d5569cb6..00000000000
--- a/packages/ra-input-rich-text/src/QuillBubbleStylesheet.ts
+++ /dev/null
@@ -1,808 +0,0 @@
-// converted from vendor (node_modules/quill/dist/quill.bubble.css) using the jss cli
-export default {
- '.ql-container': {
- boxSizing: 'border-box',
- fontFamily: 'Helvetica, Arial, sans-serif',
- fontSize: 13,
- height: '100%',
- margin: 0,
- position: 'relative',
- },
- '.ql-container.ql-disabled .ql-tooltip': {
- visibility: 'hidden',
- },
- '.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before': {
- pointerEvents: 'none',
- },
- '.ql-clipboard': {
- left: -100000,
- height: 1,
- overflowY: 'hidden',
- position: 'absolute',
- top: '50%',
- },
- '.ql-clipboard p': {
- margin: '0',
- padding: '0',
- },
- '.ql-editor': {
- boxSizing: 'border-box',
- lineHeight: '1.42',
- height: '100%',
- outline: 'none',
- overflowY: 'auto',
- padding: '12px 15px',
- tabSize: '4',
- M: '4',
- textAlign: 'left',
- whiteSpace: 'pre-wrap',
- wordWrap: 'break-word',
- },
- '.ql-editor > *': {
- cursor: 'text',
- },
- '.ql-editor p, .ql-editor ol, .ql-editor ul, .ql-editor pre, .ql-editor blockquote, .ql-editor h1, .ql-editor h2, .ql-editor h3, .ql-editor h4, .ql-editor h5, .ql-editor h6': {
- margin: '0',
- padding: '0',
- counterReset:
- 'list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol, .ql-editor ul': {
- paddingLeft: '1.5em',
- },
- '.ql-editor ol > li, .ql-editor ul > li': {
- listStyleType: 'none',
- },
- '.ql-editor ul > li::before': {
- content: "'\\2022'",
- },
- '.ql-editor ul[data-checked=true], .ql-editor ul[data-checked=false]': {
- pointerEvents: 'none',
- },
- '.ql-editor ul[data-checked=true] > li *, .ql-editor ul[data-checked=false] > li *': {
- pointerEvents: 'all',
- },
- '.ql-editor ul[data-checked=true] > li::before, .ql-editor ul[data-checked=false] > li::before': {
- color: '#777',
- cursor: 'pointer',
- pointerEvents: 'all',
- },
- '.ql-editor ul[data-checked=true] > li::before': {
- content: "'\\2611'",
- },
- '.ql-editor ul[data-checked=false] > li::before': {
- content: "'\\2610'",
- },
- '.ql-editor li::before': {
- display: 'inline-block',
- whiteSpace: 'nowrap',
- width: '1.2em',
- },
- '.ql-editor li:not(.ql-direction-rtl)::before': {
- marginLeft: '-1.5em',
- marginRight: '0.3em',
- textAlign: 'right',
- },
- '.ql-editor li.ql-direction-rtl::before': {
- marginLeft: '0.3em',
- marginRight: '-1.5em',
- },
- '.ql-editor ol li:not(.ql-direction-rtl), .ql-editor ul li:not(.ql-direction-rtl)': {
- paddingLeft: '1.5em',
- },
- '.ql-editor ol li.ql-direction-rtl, .ql-editor ul li.ql-direction-rtl': {
- paddingRight: '1.5em',
- },
- '.ql-editor ol li': {
- counterReset:
- 'list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9',
- counterIncrement: 'list-0',
- },
- '.ql-editor ol li:before': {
- content: "counter(list-0, decimal) '. '",
- },
- '.ql-editor ol li.ql-indent-1': {
- counterIncrement: 'list-1',
- counterReset: 'list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-1:before': {
- content: "counter(list-1, lower-alpha) '. '",
- },
- '.ql-editor ol li.ql-indent-2': {
- counterIncrement: 'list-2',
- counterReset: 'list-3 list-4 list-5 list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-2:before': {
- content: "counter(list-2, lower-roman) '. '",
- },
- '.ql-editor ol li.ql-indent-3': {
- counterIncrement: 'list-3',
- counterReset: 'list-4 list-5 list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-3:before': {
- content: "counter(list-3, decimal) '. '",
- },
- '.ql-editor ol li.ql-indent-4': {
- counterIncrement: 'list-4',
- counterReset: 'list-5 list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-4:before': {
- content: "counter(list-4, lower-alpha) '. '",
- },
- '.ql-editor ol li.ql-indent-5': {
- counterIncrement: 'list-5',
- counterReset: 'list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-5:before': {
- content: "counter(list-5, lower-roman) '. '",
- },
- '.ql-editor ol li.ql-indent-6': {
- counterIncrement: 'list-6',
- counterReset: 'list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-6:before': {
- content: "counter(list-6, decimal) '. '",
- },
- '.ql-editor ol li.ql-indent-7': {
- counterIncrement: 'list-7',
- counterReset: 'list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-7:before': {
- content: "counter(list-7, lower-alpha) '. '",
- },
- '.ql-editor ol li.ql-indent-8': {
- counterIncrement: 'list-8',
- counterReset: 'list-9',
- },
- '.ql-editor ol li.ql-indent-8:before': {
- content: "counter(list-8, lower-roman) '. '",
- },
- '.ql-editor ol li.ql-indent-9': {
- counterIncrement: 'list-9',
- },
- '.ql-editor ol li.ql-indent-9:before': {
- content: "counter(list-9, decimal) '. '",
- },
- '.ql-editor .ql-indent-1:not(.ql-direction-rtl)': {
- paddingLeft: '3em',
- },
- '.ql-editor li.ql-indent-1:not(.ql-direction-rtl)': {
- paddingLeft: '4.5em',
- },
- '.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right': {
- paddingRight: '3em',
- },
- '.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right': {
- paddingRight: '4.5em',
- },
- '.ql-editor .ql-indent-2:not(.ql-direction-rtl)': {
- paddingLeft: '6em',
- },
- '.ql-editor li.ql-indent-2:not(.ql-direction-rtl)': {
- paddingLeft: '7.5em',
- },
- '.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right': {
- paddingRight: '6em',
- },
- '.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right': {
- paddingRight: '7.5em',
- },
- '.ql-editor .ql-indent-3:not(.ql-direction-rtl)': {
- paddingLeft: '9em',
- },
- '.ql-editor li.ql-indent-3:not(.ql-direction-rtl)': {
- paddingLeft: '10.5em',
- },
- '.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right': {
- paddingRight: '9em',
- },
- '.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right': {
- paddingRight: '10.5em',
- },
- '.ql-editor .ql-indent-4:not(.ql-direction-rtl)': {
- paddingLeft: '12em',
- },
- '.ql-editor li.ql-indent-4:not(.ql-direction-rtl)': {
- paddingLeft: '13.5em',
- },
- '.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right': {
- paddingRight: '12em',
- },
- '.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right': {
- paddingRight: '13.5em',
- },
- '.ql-editor .ql-indent-5:not(.ql-direction-rtl)': {
- paddingLeft: '15em',
- },
- '.ql-editor li.ql-indent-5:not(.ql-direction-rtl)': {
- paddingLeft: '16.5em',
- },
- '.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right': {
- paddingRight: '15em',
- },
- '.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right': {
- paddingRight: '16.5em',
- },
- '.ql-editor .ql-indent-6:not(.ql-direction-rtl)': {
- paddingLeft: '18em',
- },
- '.ql-editor li.ql-indent-6:not(.ql-direction-rtl)': {
- paddingLeft: '19.5em',
- },
- '.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right': {
- paddingRight: '18em',
- },
- '.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right': {
- paddingRight: '19.5em',
- },
- '.ql-editor .ql-indent-7:not(.ql-direction-rtl)': {
- paddingLeft: '21em',
- },
- '.ql-editor li.ql-indent-7:not(.ql-direction-rtl)': {
- paddingLeft: '22.5em',
- },
- '.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right': {
- paddingRight: '21em',
- },
- '.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right': {
- paddingRight: '22.5em',
- },
- '.ql-editor .ql-indent-8:not(.ql-direction-rtl)': {
- paddingLeft: '24em',
- },
- '.ql-editor li.ql-indent-8:not(.ql-direction-rtl)': {
- paddingLeft: '25.5em',
- },
- '.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right': {
- paddingRight: '24em',
- },
- '.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right': {
- paddingRight: '25.5em',
- },
- '.ql-editor .ql-indent-9:not(.ql-direction-rtl)': {
- paddingLeft: '27em',
- },
- '.ql-editor li.ql-indent-9:not(.ql-direction-rtl)': {
- paddingLeft: '28.5em',
- },
- '.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right': {
- paddingRight: '27em',
- },
- '.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right': {
- paddingRight: '28.5em',
- },
- '.ql-editor .ql-video': {
- display: 'block',
- maxWidth: '100%',
- },
- '.ql-editor .ql-video.ql-align-center': {
- margin: '0 auto',
- },
- '.ql-editor .ql-video.ql-align-right': {
- margin: '0 0 0 auto',
- },
- '.ql-editor .ql-bg-black': {
- backgroundColor: '#000',
- },
- '.ql-editor .ql-bg-red': {
- backgroundColor: '#e60000',
- },
- '.ql-editor .ql-bg-orange': {
- backgroundColor: '#f90',
- },
- '.ql-editor .ql-bg-yellow': {
- backgroundColor: '#ff0',
- },
- '.ql-editor .ql-bg-green': {
- backgroundColor: '#008a00',
- },
- '.ql-editor .ql-bg-blue': {
- backgroundColor: '#06c',
- },
- '.ql-editor .ql-bg-purple': {
- backgroundColor: '#93f',
- },
- '.ql-editor .ql-color-white': {
- color: '#fff',
- },
- '.ql-editor .ql-color-red': {
- color: '#e60000',
- },
- '.ql-editor .ql-color-orange': {
- color: '#f90',
- },
- '.ql-editor .ql-color-yellow': {
- color: '#ff0',
- },
- '.ql-editor .ql-color-green': {
- color: '#008a00',
- },
- '.ql-editor .ql-color-blue': {
- color: '#06c',
- },
- '.ql-editor .ql-color-purple': {
- color: '#93f',
- },
- '.ql-editor .ql-font-serif': {
- fontFamily: 'Georgia, Times New Roman, serif',
- },
- '.ql-editor .ql-font-monospace': {
- fontFamily: 'Monaco, Courier New, monospace',
- },
- '.ql-editor .ql-size-small': {
- fontSize: '0.75em',
- },
- '.ql-editor .ql-size-large': {
- fontSize: '1.5em',
- },
- '.ql-editor .ql-size-huge': {
- fontSize: '2.5em',
- },
- '.ql-editor .ql-direction-rtl': {
- direction: 'rtl',
- textAlign: 'inherit',
- },
- '.ql-editor .ql-align-center': {
- textAlign: 'center',
- },
- '.ql-editor .ql-align-justify': {
- textAlign: 'justify',
- },
- '.ql-editor .ql-align-right': {
- textAlign: 'right',
- },
- '.ql-editor.ql-blank::before': {
- color: 'rgba(0,0,0,0.6)',
- content: 'attr(data-placeholder)',
- fontStyle: 'italic',
- left: 15,
- pointerEvents: 'none',
- position: 'absolute',
- right: 15,
- },
- '.ql-bubble.ql-toolbar:after, .ql-bubble .ql-toolbar:after': {
- clear: 'both',
- content: "''",
- display: 'table',
- },
- '.ql-bubble.ql-toolbar button, .ql-bubble .ql-toolbar button': {
- background: 'none',
- border: 'none',
- cursor: 'pointer',
- display: 'inline-block',
- float: 'left',
- height: 24,
- padding: '3px 5px',
- width: 28,
- },
- '.ql-bubble.ql-toolbar button svg, .ql-bubble .ql-toolbar button svg': {
- float: 'left',
- height: '100%',
- },
- '.ql-bubble.ql-toolbar button:active:hover, .ql-bubble .ql-toolbar button:active:hover': {
- outline: 'none',
- },
- '.ql-bubble.ql-toolbar input.ql-image[type=file], .ql-bubble .ql-toolbar input.ql-image[type=file]': {
- display: 'none',
- },
- '.ql-bubble.ql-toolbar button:hover, .ql-bubble .ql-toolbar button:hover, .ql-bubble.ql-toolbar button:focus, .ql-bubble .ql-toolbar button:focus, .ql-bubble.ql-toolbar button.ql-active, .ql-bubble .ql-toolbar button.ql-active, .ql-bubble.ql-toolbar .ql-picker-label:hover, .ql-bubble .ql-toolbar .ql-picker-label:hover, .ql-bubble.ql-toolbar .ql-picker-label.ql-active, .ql-bubble .ql-toolbar .ql-picker-label.ql-active, .ql-bubble.ql-toolbar .ql-picker-item:hover, .ql-bubble .ql-toolbar .ql-picker-item:hover, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected': {
- color: '#fff',
- },
- '.ql-bubble.ql-toolbar button:hover .ql-fill, .ql-bubble .ql-toolbar button:hover .ql-fill, .ql-bubble.ql-toolbar button:focus .ql-fill, .ql-bubble .ql-toolbar button:focus .ql-fill, .ql-bubble.ql-toolbar button.ql-active .ql-fill, .ql-bubble .ql-toolbar button.ql-active .ql-fill, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-fill, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-fill, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-fill, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-fill, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-fill, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-fill, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-fill, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-fill, .ql-bubble.ql-toolbar button:hover .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button:hover .ql-stroke.ql-fill, .ql-bubble.ql-toolbar button:focus .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button:focus .ql-stroke.ql-fill, .ql-bubble.ql-toolbar button.ql-active .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button.ql-active .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill': {
- fill: '#fff',
- },
- '.ql-bubble.ql-toolbar button:hover .ql-stroke, .ql-bubble .ql-toolbar button:hover .ql-stroke, .ql-bubble.ql-toolbar button:focus .ql-stroke, .ql-bubble .ql-toolbar button:focus .ql-stroke, .ql-bubble.ql-toolbar button.ql-active .ql-stroke, .ql-bubble .ql-toolbar button.ql-active .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-bubble.ql-toolbar button:hover .ql-stroke-miter, .ql-bubble .ql-toolbar button:hover .ql-stroke-miter, .ql-bubble.ql-toolbar button:focus .ql-stroke-miter, .ql-bubble .ql-toolbar button:focus .ql-stroke-miter, .ql-bubble.ql-toolbar button.ql-active .ql-stroke-miter, .ql-bubble .ql-toolbar button.ql-active .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter': {
- stroke: '#fff',
- },
- '@media (pointer: coarse)': {
- '.ql-bubble.ql-toolbar button:hover:not(.ql-active), .ql-bubble .ql-toolbar button:hover:not(.ql-active)': {
- color: '#ccc',
- },
- '.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-fill, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-fill, .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill': {
- fill: '#ccc',
- },
- '.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke, .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter': {
- stroke: '#ccc',
- },
- },
- '.ql-bubble': {
- boxSizing: 'border-box',
- },
- '.ql-bubble *': {
- boxSizing: 'border-box',
- },
- '.ql-bubble .ql-hidden': {
- display: 'none',
- },
- '.ql-bubble .ql-out-bottom, .ql-bubble .ql-out-top': {
- visibility: 'hidden',
- },
- '.ql-bubble .ql-tooltip': {
- position: 'absolute',
- transform: 'translateY(10px)',
- backgroundColor: '#444',
- borderRadius: 25,
- color: '#fff',
- },
- '.ql-bubble .ql-tooltip a': {
- cursor: 'pointer',
- textDecoration: 'none',
- },
- '.ql-bubble .ql-tooltip.ql-flip': {
- transform: 'translateY(-10px)',
- },
- '.ql-bubble .ql-formats': {
- display: 'inline-block',
- verticalAlign: 'middle',
- },
- '.ql-bubble .ql-formats:after': {
- clear: 'both',
- content: "''",
- display: 'table',
- },
- '.ql-bubble .ql-stroke': {
- fill: 'none',
- stroke: '#ccc',
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- strokeWidth: '2',
- },
- '.ql-bubble .ql-stroke-miter': {
- fill: 'none',
- stroke: '#ccc',
- strokeMiterlimit: 10,
- strokeWidth: '2',
- },
- '.ql-bubble .ql-fill, .ql-bubble .ql-stroke.ql-fill': {
- fill: '#ccc',
- },
- '.ql-bubble .ql-empty': {
- fill: 'none',
- },
- '.ql-bubble .ql-even': {
- fillRule: 'evenodd',
- },
- '.ql-bubble .ql-thin, .ql-bubble .ql-stroke.ql-thin': {
- strokeWidth: '1',
- },
- '.ql-bubble .ql-transparent': {
- opacity: 0.4,
- },
- '.ql-bubble .ql-direction svg:last-child': {
- display: 'none',
- },
- '.ql-bubble .ql-direction.ql-active svg:last-child': {
- display: 'inline',
- },
- '.ql-bubble .ql-direction.ql-active svg:first-of-type': {
- display: 'none',
- },
- '.ql-bubble .ql-editor h1': {
- fontSize: '2em',
- },
- '.ql-bubble .ql-editor h2': {
- fontSize: '1.5em',
- },
- '.ql-bubble .ql-editor h3': {
- fontSize: '1.17em',
- },
- '.ql-bubble .ql-editor h4': {
- fontSize: '1em',
- },
- '.ql-bubble .ql-editor h5': {
- fontSize: '0.83em',
- },
- '.ql-bubble .ql-editor h6': {
- fontSize: '0.67em',
- },
- '.ql-bubble .ql-editor a': {
- textDecoration: 'underline',
- },
- '.ql-bubble .ql-editor blockquote': {
- borderLeft: '4px solid #ccc',
- marginBottom: 5,
- marginTop: 5,
- paddingLeft: 16,
- },
- '.ql-bubble .ql-editor code, .ql-bubble .ql-editor pre': {
- backgroundColor: '#f0f0f0',
- borderRadius: 3,
- },
- '.ql-bubble .ql-editor pre': {
- whiteSpace: 'pre-wrap',
- marginBottom: 5,
- marginTop: 5,
- padding: '5px 10px',
- },
- '.ql-bubble .ql-editor code': {
- fontSize: '85%',
- padding: '2px 4px',
- },
- '.ql-bubble .ql-editor pre.ql-syntax': {
- backgroundColor: '#23241f',
- color: '#f8f8f2',
- overflow: 'visible',
- },
- '.ql-bubble .ql-editor img': {
- maxWidth: '100%',
- },
- '.ql-bubble .ql-picker': {
- color: '#ccc',
- display: 'inline-block',
- float: 'left',
- fontSize: 14,
- fontWeight: 500,
- height: 24,
- position: 'relative',
- verticalAlign: 'middle',
- },
- '.ql-bubble .ql-picker-label': {
- cursor: 'pointer',
- display: 'inline-block',
- height: '100%',
- paddingLeft: 8,
- paddingRight: 2,
- position: 'relative',
- width: '100%',
- },
- '.ql-bubble .ql-picker-label::before': {
- display: 'inline-block',
- lineHeight: 22,
- },
- '.ql-bubble .ql-picker-options': {
- backgroundColor: '#444',
- display: 'none',
- minWidth: '100%',
- padding: '4px 8px',
- position: 'absolute',
- whiteSpace: 'nowrap',
- },
- '.ql-bubble .ql-picker-options .ql-picker-item': {
- cursor: 'pointer',
- display: 'block',
- paddingBottom: 5,
- paddingTop: 5,
- },
- '.ql-bubble .ql-picker.ql-expanded .ql-picker-label': {
- color: '#777',
- zIndex: 2,
- },
- '.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-fill': {
- fill: '#777',
- },
- '.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-stroke': {
- stroke: '#777',
- },
- '.ql-bubble .ql-picker.ql-expanded .ql-picker-options': {
- display: 'block',
- marginTop: -1,
- top: '100%',
- zIndex: 1,
- },
- '.ql-bubble .ql-color-picker, .ql-bubble .ql-icon-picker': {
- width: 28,
- },
- '.ql-bubble .ql-color-picker .ql-picker-label, .ql-bubble .ql-icon-picker .ql-picker-label': {
- padding: '2px 4px',
- },
- '.ql-bubble .ql-color-picker .ql-picker-label svg, .ql-bubble .ql-icon-picker .ql-picker-label svg': {
- right: 4,
- },
- '.ql-bubble .ql-icon-picker .ql-picker-options': {
- padding: '4px 0px',
- },
- '.ql-bubble .ql-icon-picker .ql-picker-item': {
- height: 24,
- width: 24,
- padding: '2px 4px',
- },
- '.ql-bubble .ql-color-picker .ql-picker-options': {
- padding: '3px 5px',
- width: 152,
- },
- '.ql-bubble .ql-color-picker .ql-picker-item': {
- border: '1px solid transparent',
- float: 'left',
- height: 16,
- margin: 2,
- padding: 0,
- width: 16,
- },
- '.ql-bubble .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg': {
- position: 'absolute',
- marginTop: -9,
- right: '0',
- top: '50%',
- width: 18,
- },
- ".ql-bubble .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before": {
- content: 'attr(data-label)',
- },
- '.ql-bubble .ql-picker.ql-header': {
- width: 98,
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-label::before, .ql-bubble .ql-picker.ql-header .ql-picker-item::before': {
- content: "'Normal'",
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="1"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before': {
- content: "'Heading 1'",
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="2"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before': {
- content: "'Heading 2'",
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="3"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before': {
- content: "'Heading 3'",
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="4"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before': {
- content: "'Heading 4'",
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="5"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before': {
- content: "'Heading 5'",
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="6"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before': {
- content: "'Heading 6'",
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before': {
- fontSize: '2em',
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before': {
- fontSize: '1.5em',
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before': {
- fontSize: '1.17em',
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before': {
- fontSize: '1em',
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before': {
- fontSize: '0.83em',
- },
- '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before': {
- fontSize: '0.67em',
- },
- '.ql-bubble .ql-picker.ql-font': {
- width: 108,
- },
- '.ql-bubble .ql-picker.ql-font .ql-picker-label::before, .ql-bubble .ql-picker.ql-font .ql-picker-item::before': {
- content: "'Sans Serif'",
- },
- '.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=serif]::before, .ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before': {
- content: "'Serif'",
- },
- '.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before, .ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before': {
- content: "'Monospace'",
- },
- '.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before': {
- fontFamily: 'Georgia, Times New Roman, serif',
- },
- '.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before': {
- fontFamily: 'Monaco, Courier New, monospace',
- },
- '.ql-bubble .ql-picker.ql-size': {
- width: 98,
- },
- '.ql-bubble .ql-picker.ql-size .ql-picker-label::before, .ql-bubble .ql-picker.ql-size .ql-picker-item::before': {
- content: "'Normal'",
- },
- '.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=small]::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before': {
- content: "'Small'",
- },
- '.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=large]::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before': {
- content: "'Large'",
- },
- '.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=huge]::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before': {
- content: "'Huge'",
- },
- '.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before': {
- fontSize: 10,
- },
- '.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before': {
- fontSize: 18,
- },
- '.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before': {
- fontSize: 32,
- },
- '.ql-bubble .ql-color-picker.ql-background .ql-picker-item': {
- backgroundColor: '#fff',
- },
- '.ql-bubble .ql-color-picker.ql-color .ql-picker-item': {
- backgroundColor: '#000',
- },
- '.ql-bubble .ql-toolbar .ql-formats': {
- margin: '8px 12px 8px 0px',
- },
- '.ql-bubble .ql-toolbar .ql-formats:first-of-type': {
- marginLeft: 12,
- },
- '.ql-bubble .ql-color-picker svg': {
- margin: 1,
- },
- '.ql-bubble .ql-color-picker .ql-picker-item.ql-selected, .ql-bubble .ql-color-picker .ql-picker-item:hover': {
- borderColor: '#fff',
- },
- '.ql-bubble .ql-tooltip-arrow': {
- borderLeft: '6px solid transparent',
- borderRight: '6px solid transparent',
- content: '" "',
- display: 'block',
- left: '50%',
- marginLeft: -6,
- position: 'absolute',
- },
- '.ql-bubble .ql-tooltip:not(.ql-flip) .ql-tooltip-arrow': {
- borderBottom: '6px solid #444',
- top: -6,
- },
- '.ql-bubble .ql-tooltip.ql-flip .ql-tooltip-arrow': {
- borderTop: '6px solid #444',
- bottom: -6,
- },
- '.ql-bubble .ql-tooltip.ql-editing .ql-tooltip-editor': {
- display: 'block',
- },
- '.ql-bubble .ql-tooltip.ql-editing .ql-formats': {
- visibility: 'hidden',
- },
- '.ql-bubble .ql-tooltip-editor': {
- display: 'none',
- },
- '.ql-bubble .ql-tooltip-editor input[type=text]': {
- background: 'transparent',
- border: 'none',
- color: '#fff',
- fontSize: 13,
- height: '100%',
- outline: 'none',
- padding: '10px 20px',
- position: 'absolute',
- width: '100%',
- },
- '.ql-bubble .ql-tooltip-editor a': {
- top: 10,
- position: 'absolute',
- right: 20,
- },
- '.ql-bubble .ql-tooltip-editor a:before': {
- color: '#ccc',
- content: '"D7"',
- fontSize: 16,
- fontWeight: 'bold',
- },
- '.ql-container.ql-bubble:not(.ql-disabled) a': {
- position: 'relative',
- whiteSpace: 'nowrap',
- },
- '.ql-container.ql-bubble:not(.ql-disabled) a::before': {
- backgroundColor: '#444',
- borderRadius: 15,
- top: -5,
- fontSize: 12,
- color: '#fff',
- content: 'attr(href)',
- fontWeight: 'normal',
- overflow: 'hidden',
- padding: '5px 15px',
- textDecoration: 'none',
- zIndex: 1,
- },
- '.ql-container.ql-bubble:not(.ql-disabled) a::after': {
- borderTop: '6px solid #444',
- borderLeft: '6px solid transparent',
- borderRight: '6px solid transparent',
- top: '0',
- content: '" "',
- height: '0',
- width: '0',
- },
- '.ql-container.ql-bubble:not(.ql-disabled) a::before, .ql-container.ql-bubble:not(.ql-disabled) a::after': {
- left: '0',
- marginLeft: '50%',
- position: 'absolute',
- transform: 'translate(-50%, -100%)',
- transition: 'visibility 0s ease 200ms',
- visibility: 'hidden',
- },
- '.ql-container.ql-bubble:not(.ql-disabled) a:hover::before, .ql-container.ql-bubble:not(.ql-disabled) a:hover::after': {
- visibility: 'visible',
- },
-};
diff --git a/packages/ra-input-rich-text/src/QuillSnowStylesheet.ts b/packages/ra-input-rich-text/src/QuillSnowStylesheet.ts
deleted file mode 100644
index e8b0efa72a5..00000000000
--- a/packages/ra-input-rich-text/src/QuillSnowStylesheet.ts
+++ /dev/null
@@ -1,800 +0,0 @@
-// converted from vendor (node_modules/quill/dist/quill.snow.css) using the jss cli
-export default {
- '.ql-container': {
- boxSizing: 'border-box',
- fontFamily: 'Helvetica, Arial, sans-serif',
- fontSize: 13,
- height: '100%',
- margin: 0,
- position: 'relative',
- },
- '.ql-container.ql-disabled .ql-tooltip': {
- visibility: 'hidden',
- },
- '.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before': {
- pointerEvents: 'none',
- },
- '.ql-clipboard': {
- left: -100000,
- height: 1,
- overflowY: 'hidden',
- position: 'absolute',
- top: '50%',
- },
- '.ql-clipboard p': {
- margin: '0',
- padding: '0',
- },
- '.ql-editor': {
- boxSizing: 'border-box',
- lineHeight: '1.42',
- height: '100%',
- outline: 'none',
- overflowY: 'auto',
- padding: '12px 15px',
- tabSize: '4',
- textAlign: 'left',
- whiteSpace: 'pre-wrap',
- wordWrap: 'break-word',
- },
- '.ql-editor > *': {
- cursor: 'text',
- },
- '.ql-editor p, .ql-editor ol, .ql-editor ul, .ql-editor pre, .ql-editor blockquote, .ql-editor h1, .ql-editor h2, .ql-editor h3, .ql-editor h4, .ql-editor h5, .ql-editor h6': {
- margin: '0',
- padding: '0',
- counterReset:
- 'list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol, .ql-editor ul': {
- paddingLeft: '1.5em',
- },
- '.ql-editor ol > li, .ql-editor ul > li': {
- listStyleType: 'none',
- },
- '.ql-editor ul > li::before': {
- content: "'\\2022'",
- },
- '.ql-editor ul[data-checked=true], .ql-editor ul[data-checked=false]': {
- pointerEvents: 'none',
- },
- '.ql-editor ul[data-checked=true] > li *, .ql-editor ul[data-checked=false] > li *': {
- pointerEvents: 'all',
- },
- '.ql-editor ul[data-checked=true] > li::before, .ql-editor ul[data-checked=false] > li::before': {
- color: '#777',
- cursor: 'pointer',
- pointerEvents: 'all',
- },
- '.ql-editor ul[data-checked=true] > li::before': {
- content: "'\\2611'",
- },
- '.ql-editor ul[data-checked=false] > li::before': {
- content: "'\\2610'",
- },
- '.ql-editor li::before': {
- display: 'inline-block',
- whiteSpace: 'nowrap',
- width: '1.2em',
- },
- '.ql-editor li:not(.ql-direction-rtl)::before': {
- marginLeft: '-1.5em',
- marginRight: '0.3em',
- textAlign: 'right',
- },
- '.ql-editor li.ql-direction-rtl::before': {
- marginLeft: '0.3em',
- marginRight: '-1.5em',
- },
- '.ql-editor ol li:not(.ql-direction-rtl), .ql-editor ul li:not(.ql-direction-rtl)': {
- paddingLeft: '1.5em',
- },
- '.ql-editor ol li.ql-direction-rtl, .ql-editor ul li.ql-direction-rtl': {
- paddingRight: '1.5em',
- },
- '.ql-editor ol li': {
- counterReset:
- 'list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9',
- counterIncrement: 'list-0',
- },
- '.ql-editor ol li:before': {
- content: "counter(list-0, decimal) '. '",
- },
- '.ql-editor ol li.ql-indent-1': {
- counterIncrement: 'list-1',
- counterReset: 'list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-1:before': {
- content: "counter(list-1, lower-alpha) '. '",
- },
- '.ql-editor ol li.ql-indent-2': {
- counterIncrement: 'list-2',
- counterReset: 'list-3 list-4 list-5 list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-2:before': {
- content: "counter(list-2, lower-roman) '. '",
- },
- '.ql-editor ol li.ql-indent-3': {
- counterIncrement: 'list-3',
- counterReset: 'list-4 list-5 list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-3:before': {
- content: "counter(list-3, decimal) '. '",
- },
- '.ql-editor ol li.ql-indent-4': {
- counterIncrement: 'list-4',
- counterReset: 'list-5 list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-4:before': {
- content: "counter(list-4, lower-alpha) '. '",
- },
- '.ql-editor ol li.ql-indent-5': {
- counterIncrement: 'list-5',
- counterReset: 'list-6 list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-5:before': {
- content: "counter(list-5, lower-roman) '. '",
- },
- '.ql-editor ol li.ql-indent-6': {
- counterIncrement: 'list-6',
- counterReset: 'list-7 list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-6:before': {
- content: "counter(list-6, decimal) '. '",
- },
- '.ql-editor ol li.ql-indent-7': {
- counterIncrement: 'list-7',
- counterReset: 'list-8 list-9',
- },
- '.ql-editor ol li.ql-indent-7:before': {
- content: "counter(list-7, lower-alpha) '. '",
- },
- '.ql-editor ol li.ql-indent-8': {
- counterIncrement: 'list-8',
- counterReset: 'list-9',
- },
- '.ql-editor ol li.ql-indent-8:before': {
- content: "counter(list-8, lower-roman) '. '",
- },
- '.ql-editor ol li.ql-indent-9': {
- counterIncrement: 'list-9',
- },
- '.ql-editor ol li.ql-indent-9:before': {
- content: "counter(list-9, decimal) '. '",
- },
- '.ql-editor .ql-indent-1:not(.ql-direction-rtl)': {
- paddingLeft: '3em',
- },
- '.ql-editor li.ql-indent-1:not(.ql-direction-rtl)': {
- paddingLeft: '4.5em',
- },
- '.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right': {
- paddingRight: '3em',
- },
- '.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right': {
- paddingRight: '4.5em',
- },
- '.ql-editor .ql-indent-2:not(.ql-direction-rtl)': {
- paddingLeft: '6em',
- },
- '.ql-editor li.ql-indent-2:not(.ql-direction-rtl)': {
- paddingLeft: '7.5em',
- },
- '.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right': {
- paddingRight: '6em',
- },
- '.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right': {
- paddingRight: '7.5em',
- },
- '.ql-editor .ql-indent-3:not(.ql-direction-rtl)': {
- paddingLeft: '9em',
- },
- '.ql-editor li.ql-indent-3:not(.ql-direction-rtl)': {
- paddingLeft: '10.5em',
- },
- '.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right': {
- paddingRight: '9em',
- },
- '.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right': {
- paddingRight: '10.5em',
- },
- '.ql-editor .ql-indent-4:not(.ql-direction-rtl)': {
- paddingLeft: '12em',
- },
- '.ql-editor li.ql-indent-4:not(.ql-direction-rtl)': {
- paddingLeft: '13.5em',
- },
- '.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right': {
- paddingRight: '12em',
- },
- '.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right': {
- paddingRight: '13.5em',
- },
- '.ql-editor .ql-indent-5:not(.ql-direction-rtl)': {
- paddingLeft: '15em',
- },
- '.ql-editor li.ql-indent-5:not(.ql-direction-rtl)': {
- paddingLeft: '16.5em',
- },
- '.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right': {
- paddingRight: '15em',
- },
- '.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right': {
- paddingRight: '16.5em',
- },
- '.ql-editor .ql-indent-6:not(.ql-direction-rtl)': {
- paddingLeft: '18em',
- },
- '.ql-editor li.ql-indent-6:not(.ql-direction-rtl)': {
- paddingLeft: '19.5em',
- },
- '.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right': {
- paddingRight: '18em',
- },
- '.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right': {
- paddingRight: '19.5em',
- },
- '.ql-editor .ql-indent-7:not(.ql-direction-rtl)': {
- paddingLeft: '21em',
- },
- '.ql-editor li.ql-indent-7:not(.ql-direction-rtl)': {
- paddingLeft: '22.5em',
- },
- '.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right': {
- paddingRight: '21em',
- },
- '.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right': {
- paddingRight: '22.5em',
- },
- '.ql-editor .ql-indent-8:not(.ql-direction-rtl)': {
- paddingLeft: '24em',
- },
- '.ql-editor li.ql-indent-8:not(.ql-direction-rtl)': {
- paddingLeft: '25.5em',
- },
- '.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right': {
- paddingRight: '24em',
- },
- '.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right': {
- paddingRight: '25.5em',
- },
- '.ql-editor .ql-indent-9:not(.ql-direction-rtl)': {
- paddingLeft: '27em',
- },
- '.ql-editor li.ql-indent-9:not(.ql-direction-rtl)': {
- paddingLeft: '28.5em',
- },
- '.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right': {
- paddingRight: '27em',
- },
- '.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right': {
- paddingRight: '28.5em',
- },
- '.ql-editor .ql-video': {
- display: 'block',
- maxWidth: '100%',
- },
- '.ql-editor .ql-video.ql-align-center': {
- margin: '0 auto',
- },
- '.ql-editor .ql-video.ql-align-right': {
- margin: '0 0 0 auto',
- },
- '.ql-editor .ql-bg-black': {
- backgroundColor: '#000',
- },
- '.ql-editor .ql-bg-red': {
- backgroundColor: '#e60000',
- },
- '.ql-editor .ql-bg-orange': {
- backgroundColor: '#f90',
- },
- '.ql-editor .ql-bg-yellow': {
- backgroundColor: '#ff0',
- },
- '.ql-editor .ql-bg-green': {
- backgroundColor: '#008a00',
- },
- '.ql-editor .ql-bg-blue': {
- backgroundColor: '#06c',
- },
- '.ql-editor .ql-bg-purple': {
- backgroundColor: '#93f',
- },
- '.ql-editor .ql-color-white': {
- color: '#fff',
- },
- '.ql-editor .ql-color-red': {
- color: '#e60000',
- },
- '.ql-editor .ql-color-orange': {
- color: '#f90',
- },
- '.ql-editor .ql-color-yellow': {
- color: '#ff0',
- },
- '.ql-editor .ql-color-green': {
- color: '#008a00',
- },
- '.ql-editor .ql-color-blue': {
- color: '#06c',
- },
- '.ql-editor .ql-color-purple': {
- color: '#93f',
- },
- '.ql-editor .ql-font-serif': {
- fontFamily: 'Georgia, Times New Roman, serif',
- },
- '.ql-editor .ql-font-monospace': {
- fontFamily: 'Monaco, Courier New, monospace',
- },
- '.ql-editor .ql-size-small': {
- fontSize: '0.75em',
- },
- '.ql-editor .ql-size-large': {
- fontSize: '1.5em',
- },
- '.ql-editor .ql-size-huge': {
- fontSize: '2.5em',
- },
- '.ql-editor .ql-direction-rtl': {
- direction: 'rtl',
- textAlign: 'inherit',
- },
- '.ql-editor .ql-align-center': {
- textAlign: 'center',
- },
- '.ql-editor .ql-align-justify': {
- textAlign: 'justify',
- },
- '.ql-editor .ql-align-right': {
- textAlign: 'right',
- },
- '.ql-editor.ql-blank::before': {
- color: 'rgba(0,0,0,0.6)',
- content: 'attr(data-placeholder)',
- fontStyle: 'italic',
- left: 15,
- pointerEvents: 'none',
- position: 'absolute',
- right: 15,
- },
- '.ql-snow.ql-toolbar:after, .ql-snow .ql-toolbar:after': {
- clear: 'both',
- content: "''",
- display: 'table',
- },
- '.ql-snow.ql-toolbar button, .ql-snow .ql-toolbar button': {
- background: 'none',
- border: 'none',
- cursor: 'pointer',
- display: 'inline-block',
- float: 'left',
- height: 24,
- padding: '3px 5px',
- width: 28,
- },
- '.ql-snow.ql-toolbar button svg, .ql-snow .ql-toolbar button svg': {
- float: 'left',
- height: '100%',
- },
- '.ql-snow.ql-toolbar button:active:hover, .ql-snow .ql-toolbar button:active:hover': {
- outline: 'none',
- },
- '.ql-snow.ql-toolbar input.ql-image[type=file], .ql-snow .ql-toolbar input.ql-image[type=file]': {
- display: 'none',
- },
- '.ql-snow.ql-toolbar button:hover, .ql-snow .ql-toolbar button:hover, .ql-snow.ql-toolbar button:focus, .ql-snow .ql-toolbar button:focus, .ql-snow.ql-toolbar button.ql-active, .ql-snow .ql-toolbar button.ql-active, .ql-snow.ql-toolbar .ql-picker-label:hover, .ql-snow .ql-toolbar .ql-picker-label:hover, .ql-snow.ql-toolbar .ql-picker-label.ql-active, .ql-snow .ql-toolbar .ql-picker-label.ql-active, .ql-snow.ql-toolbar .ql-picker-item:hover, .ql-snow .ql-toolbar .ql-picker-item:hover, .ql-snow.ql-toolbar .ql-picker-item.ql-selected, .ql-snow .ql-toolbar .ql-picker-item.ql-selected': {
- color: '#06c',
- },
- '.ql-snow.ql-toolbar button:hover .ql-fill, .ql-snow .ql-toolbar button:hover .ql-fill, .ql-snow.ql-toolbar button:focus .ql-fill, .ql-snow .ql-toolbar button:focus .ql-fill, .ql-snow.ql-toolbar button.ql-active .ql-fill, .ql-snow .ql-toolbar button.ql-active .ql-fill, .ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill, .ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill, .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill, .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill, .ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill, .ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill, .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill, .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill, .ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill, .ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill, .ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill, .ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill, .ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill, .ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill, .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill, .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill': {
- fill: '#06c',
- },
- '.ql-snow.ql-toolbar button:hover .ql-stroke, .ql-snow .ql-toolbar button:hover .ql-stroke, .ql-snow.ql-toolbar button:focus .ql-stroke, .ql-snow .ql-toolbar button:focus .ql-stroke, .ql-snow.ql-toolbar button.ql-active .ql-stroke, .ql-snow .ql-toolbar button.ql-active .ql-stroke, .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-snow.ql-toolbar button:hover .ql-stroke-miter, .ql-snow .ql-toolbar button:hover .ql-stroke-miter, .ql-snow.ql-toolbar button:focus .ql-stroke-miter, .ql-snow .ql-toolbar button:focus .ql-stroke-miter, .ql-snow.ql-toolbar button.ql-active .ql-stroke-miter, .ql-snow .ql-toolbar button.ql-active .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter': {
- stroke: '#06c',
- },
- '@media (pointer: coarse)': {
- '.ql-snow.ql-toolbar button:hover:not(.ql-active), .ql-snow .ql-toolbar button:hover:not(.ql-active)': {
- color: '#444',
- },
- '.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill, .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill, .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill, .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill': {
- fill: '#444',
- },
- '.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke, .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke, .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter, .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter': {
- stroke: '#444',
- },
- },
- '.ql-snow': {
- boxSizing: 'border-box',
- },
- '.ql-snow *': {
- boxSizing: 'border-box',
- },
- '.ql-snow .ql-hidden': {
- display: 'none',
- },
- '.ql-snow .ql-out-bottom, .ql-snow .ql-out-top': {
- visibility: 'hidden',
- },
- '.ql-snow .ql-tooltip': {
- position: 'absolute',
- transform: 'translateY(10px)',
- backgroundColor: '#fff',
- border: '1px solid #ccc',
- boxShadow: '0px 0px 5px #ddd',
- color: '#444',
- padding: '5px 12px',
- whiteSpace: 'nowrap',
- zIndex: 1,
- },
- '.ql-snow .ql-tooltip a': {
- cursor: 'pointer',
- textDecoration: 'none',
- lineHeight: '26px',
- },
- '.ql-snow .ql-tooltip.ql-flip': {
- transform: 'translateY(-10px)',
- },
- '.ql-snow .ql-formats': {
- display: 'inline-block',
- verticalAlign: 'middle',
- },
- '.ql-snow .ql-formats:after': {
- clear: 'both',
- content: "''",
- display: 'table',
- },
- '.ql-snow .ql-stroke': {
- fill: 'none',
- stroke: '#444',
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- strokeWidth: '2',
- },
- '.ql-snow .ql-stroke-miter': {
- fill: 'none',
- stroke: '#444',
- strokeMiterlimit: 10,
- strokeWidth: '2',
- },
- '.ql-snow .ql-fill, .ql-snow .ql-stroke.ql-fill': {
- fill: '#444',
- },
- '.ql-snow .ql-empty': {
- fill: 'none',
- },
- '.ql-snow .ql-even': {
- fillRule: 'evenodd',
- },
- '.ql-snow .ql-thin, .ql-snow .ql-stroke.ql-thin': {
- strokeWidth: '1',
- },
- '.ql-snow .ql-transparent': {
- opacity: 0.4,
- },
- '.ql-snow .ql-direction svg:last-child': {
- display: 'none',
- },
- '.ql-snow .ql-direction.ql-active svg:last-child': {
- display: 'inline',
- },
- '.ql-snow .ql-direction.ql-active svg:first-of-type': {
- display: 'none',
- },
- '.ql-snow .ql-editor h1': {
- fontSize: '2em',
- },
- '.ql-snow .ql-editor h2': {
- fontSize: '1.5em',
- },
- '.ql-snow .ql-editor h3': {
- fontSize: '1.17em',
- },
- '.ql-snow .ql-editor h4': {
- fontSize: '1em',
- },
- '.ql-snow .ql-editor h5': {
- fontSize: '0.83em',
- },
- '.ql-snow .ql-editor h6': {
- fontSize: '0.67em',
- },
- '.ql-snow .ql-editor a': {
- textDecoration: 'underline',
- },
- '.ql-snow .ql-editor blockquote': {
- borderLeft: '4px solid #ccc',
- marginBottom: 5,
- marginTop: 5,
- paddingLeft: 16,
- },
- '.ql-snow .ql-editor code, .ql-snow .ql-editor pre': {
- backgroundColor: '#f0f0f0',
- borderRadius: 3,
- },
- '.ql-snow .ql-editor pre': {
- whiteSpace: 'pre-wrap',
- marginBottom: 5,
- marginTop: 5,
- padding: '5px 10px',
- },
- '.ql-snow .ql-editor code': {
- fontSize: '85%',
- padding: '2px 4px',
- },
- '.ql-snow .ql-editor pre.ql-syntax': {
- backgroundColor: '#23241f',
- color: '#f8f8f2',
- overflow: 'visible',
- },
- '.ql-snow .ql-editor img': {
- maxWidth: '100%',
- },
- '.ql-snow .ql-picker': {
- color: '#444',
- display: 'inline-block',
- float: 'left',
- fontSize: 14,
- fontWeight: 500,
- height: 24,
- position: 'relative',
- verticalAlign: 'middle',
- },
- '.ql-snow .ql-picker-label': {
- cursor: 'pointer',
- display: 'inline-block',
- height: '100%',
- paddingLeft: 8,
- paddingRight: 2,
- position: 'relative',
- width: '100%',
- },
- '.ql-snow .ql-picker-label::before': {
- display: 'inline-block',
- lineHeight: '22px',
- },
- '.ql-snow .ql-picker-options': {
- backgroundColor: '#fff',
- display: 'none',
- minWidth: '100%',
- padding: '4px 8px',
- position: 'absolute',
- whiteSpace: 'nowrap',
- },
- '.ql-snow .ql-picker-options .ql-picker-item': {
- cursor: 'pointer',
- display: 'block',
- paddingBottom: 5,
- paddingTop: 5,
- },
- '.ql-snow .ql-picker.ql-expanded .ql-picker-label': {
- color: '#ccc',
- zIndex: 2,
- },
- '.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill': {
- fill: '#ccc',
- },
- '.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke': {
- stroke: '#ccc',
- },
- '.ql-snow .ql-picker.ql-expanded .ql-picker-options': {
- display: 'block',
- marginTop: -1,
- top: '100%',
- zIndex: 1,
- },
- '.ql-snow .ql-color-picker, .ql-snow .ql-icon-picker': {
- width: 28,
- },
- '.ql-snow .ql-color-picker .ql-picker-label, .ql-snow .ql-icon-picker .ql-picker-label': {
- padding: '2px 4px',
- },
- '.ql-snow .ql-color-picker .ql-picker-label svg, .ql-snow .ql-icon-picker .ql-picker-label svg': {
- right: 4,
- },
- '.ql-snow .ql-icon-picker .ql-picker-options': {
- padding: '4px 0px',
- },
- '.ql-snow .ql-icon-picker .ql-picker-item': {
- height: 24,
- width: 24,
- padding: '2px 4px',
- },
- '.ql-snow .ql-color-picker .ql-picker-options': {
- padding: '3px 5px',
- width: 152,
- },
- '.ql-snow .ql-color-picker .ql-picker-item': {
- border: '1px solid transparent',
- float: 'left',
- height: 16,
- margin: 2,
- padding: 0,
- width: 16,
- },
- '.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg': {
- position: 'absolute',
- marginTop: -9,
- right: '0',
- top: '50%',
- width: 18,
- },
- ".ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before, .ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before, .ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before, .ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before, .ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before": {
- content: 'attr(data-label)',
- },
- '.ql-snow .ql-picker.ql-header': {
- width: 98,
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-label::before, .ql-snow .ql-picker.ql-header .ql-picker-item::before': {
- content: "'Normal'",
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before': {
- content: "'Heading 1'",
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before': {
- content: "'Heading 2'",
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before': {
- content: "'Heading 3'",
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before': {
- content: "'Heading 4'",
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before': {
- content: "'Heading 5'",
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before': {
- content: "'Heading 6'",
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before': {
- fontSize: '2em',
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before': {
- fontSize: '1.5em',
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before': {
- fontSize: '1.17em',
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before': {
- fontSize: '1em',
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before': {
- fontSize: '0.83em',
- },
- '.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before': {
- fontSize: '0.67em',
- },
- '.ql-snow .ql-picker.ql-font': {
- width: 108,
- },
- '.ql-snow .ql-picker.ql-font .ql-picker-label::before, .ql-snow .ql-picker.ql-font .ql-picker-item::before': {
- content: "'Sans Serif'",
- },
- '.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]::before, .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before': {
- content: "'Serif'",
- },
- '.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before, .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before': {
- content: "'Monospace'",
- },
- '.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before': {
- fontFamily: 'Georgia, Times New Roman, serif',
- },
- '.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before': {
- fontFamily: 'Monaco, Courier New, monospace',
- },
- '.ql-snow .ql-picker.ql-size': {
- width: 98,
- },
- '.ql-snow .ql-picker.ql-size .ql-picker-label::before, .ql-snow .ql-picker.ql-size .ql-picker-item::before': {
- content: "'Normal'",
- },
- '.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]::before, .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before': {
- content: "'Small'",
- },
- '.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]::before, .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before': {
- content: "'Large'",
- },
- '.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]::before, .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before': {
- content: "'Huge'",
- },
- '.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before': {
- fontSize: 10,
- },
- '.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before': {
- fontSize: 18,
- },
- '.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before': {
- fontSize: 32,
- },
- '.ql-snow .ql-color-picker.ql-background .ql-picker-item': {
- backgroundColor: '#fff',
- },
- '.ql-snow .ql-color-picker.ql-color .ql-picker-item': {
- backgroundColor: '#000',
- },
- '.ql-toolbar.ql-snow': {
- border: '1px solid #ccc',
- boxSizing: 'border-box',
- fontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
- padding: 8,
- },
- '.ql-toolbar.ql-snow .ql-formats': {
- marginRight: 15,
- },
- '.ql-toolbar.ql-snow .ql-picker-label': {
- border: '1px solid transparent',
- },
- '.ql-toolbar.ql-snow .ql-picker-options': {
- border: '1px solid transparent',
- boxShadow: 'rgba(0,0,0,0.2) 0 2px 8px',
- },
- '.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label': {
- borderColor: '#ccc',
- },
- '.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options': {
- borderColor: '#ccc',
- },
- '.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected, .ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover': {
- borderColor: '#000',
- },
- '.ql-toolbar.ql-snow + .ql-container.ql-snow': {
- borderTop: 0,
- },
- '.ql-snow .ql-tooltip::before': {
- content: '"Visit URL:"',
- lineHeight: '26px',
- marginRight: 8,
- },
- '.ql-snow .ql-tooltip input[type=text]': {
- display: 'none',
- border: '1px solid #ccc',
- fontSize: 13,
- height: 26,
- margin: 0,
- padding: '3px 5px',
- width: 170,
- },
- '.ql-snow .ql-tooltip a.ql-preview': {
- display: 'inline-block',
- maxWidth: 200,
- overflowX: 'hidden',
- textOverflow: 'ellipsis',
- verticalAlign: 'top',
- },
- '.ql-snow .ql-tooltip a.ql-action::after': {
- borderRight: '1px solid #ccc',
- content: "'Edit'",
- marginLeft: 16,
- paddingRight: 8,
- },
- '.ql-snow .ql-tooltip a.ql-remove::before': {
- content: "'Remove'",
- marginLeft: 8,
- },
- '.ql-snow .ql-tooltip.ql-editing a.ql-preview, .ql-snow .ql-tooltip.ql-editing a.ql-remove': {
- display: 'none',
- },
- '.ql-snow .ql-tooltip.ql-editing input[type=text]': {
- display: 'inline-block',
- },
- '.ql-snow .ql-tooltip.ql-editing a.ql-action::after': {
- borderRight: 0,
- content: "'Save'",
- paddingRight: 0,
- },
- '.ql-snow .ql-tooltip[data-mode=link]::before': {
- content: '"Enter link:"',
- },
- '.ql-snow .ql-tooltip[data-mode=formula]::before': {
- content: '"Enter formula:"',
- },
- '.ql-snow .ql-tooltip[data-mode=video]::before': {
- content: '"Enter video:"',
- },
- '.ql-snow a': {
- color: '#06c',
- },
- '.ql-container.ql-snow': {
- border: '1px solid #ccc',
- },
-};
diff --git a/packages/ra-input-rich-text/src/RichTextInput.spec.js b/packages/ra-input-rich-text/src/RichTextInput.spec.js
deleted file mode 100644
index efcdcc35f53..00000000000
--- a/packages/ra-input-rich-text/src/RichTextInput.spec.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as React from 'react';
-import debounce from 'lodash/debounce';
-import { fireEvent, render, screen, waitFor } from '@testing-library/react';
-import { testDataProvider } from 'ra-core';
-import { AdminContext, SimpleForm } from 'ra-ui-materialui';
-
-import { RichTextInput } from './RichTextInput';
-
-let container;
-
-jest.mock('lodash/debounce');
-
-describe('RichTextInput', () => {
- beforeEach(() => {
- container = document.createElement('div');
- document.body.appendChild(container);
- // required as quilljs uses getSelection api
- document.getSelection = () => {
- return {
- removeAllRanges: () => {},
- getRangeAt: function () {},
- };
- };
- });
-
- afterEach(() => {
- document.body.removeChild(container);
- container = null;
- });
-
- it('should call handleChange only once when editing', async () => {
- const handleChange = jest.fn();
- debounce.mockImplementation(fn => fn);
- render(
-
- test
' }}
- onSubmit={jest.fn()}
- >
-
-
-
- );
- const quillNode = await waitFor(() => {
- return screen.getByTestId('quill');
- });
- const node = quillNode.querySelector('.ql-editor');
- fireEvent.input(node, {
- target: { innerHTML: 'test1
' },
- });
-
- await waitFor(() => {
- expect(handleChange).toHaveBeenCalledTimes(1);
- });
- });
-});
diff --git a/packages/ra-input-rich-text/src/RichTextInput.stories.tsx b/packages/ra-input-rich-text/src/RichTextInput.stories.tsx
new file mode 100644
index 00000000000..9ed51238dcd
--- /dev/null
+++ b/packages/ra-input-rich-text/src/RichTextInput.stories.tsx
@@ -0,0 +1,83 @@
+import * as React from 'react';
+import { Form, FormProps, required } from 'ra-core';
+import { AdminContext } from 'ra-ui-materialui';
+import { RichTextInput } from './RichTextInput';
+import { RichTextInputToolbar } from './RichTextInputToolbar';
+
+export default { title: 'Basic Usage' };
+
+export const Basic = (props: Partial) => (
+
+
+);
+
+export const Small = (props: Partial) => (
+
+
+);
+
+export const Large = (props: Partial) => (
+
+
+);
+
+export const Validation = (props: Partial) => (
+
+
+);
diff --git a/packages/ra-input-rich-text/src/RichTextInput.tsx b/packages/ra-input-rich-text/src/RichTextInput.tsx
index b6331ff7818..181c53f2eec 100644
--- a/packages/ra-input-rich-text/src/RichTextInput.tsx
+++ b/packages/ra-input-rich-text/src/RichTextInput.tsx
@@ -1,171 +1,272 @@
-import debounce from 'lodash/debounce';
-import React, { useRef, useEffect, useCallback, ComponentProps } from 'react';
-import Quill, { QuillOptionsStatic } from 'quill';
-import { useInput, FieldTitle } from 'ra-core';
-import { InputHelperText } from 'ra-ui-materialui';
+import * as React from 'react';
+import { ReactElement, ReactNode, useEffect } from 'react';
+import { useEditor, Editor, EditorOptions, EditorContent } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+import Underline from '@tiptap/extension-underline';
+import Link from '@tiptap/extension-link';
+import TextAlign from '@tiptap/extension-text-align';
+import { FormHelperText } from '@mui/material';
+import { styled } from '@mui/material/styles';
+import { useInput, useResourceContext } from 'ra-core';
import {
- FormHelperText,
- FormControl,
- InputLabel,
- styled,
- GlobalStyles,
-} from '@mui/material';
-import PropTypes from 'prop-types';
-
-import { RaRichTextClasses, RaRichTextStyles } from './styles';
-import QuillSnowStylesheet from './QuillSnowStylesheet';
+ CommonInputProps,
+ InputHelperText,
+ Labeled,
+ LabeledProps,
+} from 'ra-ui-materialui';
+import { TiptapEditorProvider } from './TiptapEditorProvider';
+import { RichTextInputToolbar } from './RichTextInputToolbar';
+/**
+ * A rich text editor for the react-admin that is accessible and supports translations. Based on [Tiptap](https://www.tiptap.dev/).
+ * @param props The input props. Accept all common react-admin input props.
+ * @param {EditorOptions} props.editorOptions The options to pass to the Tiptap editor.
+ * @param {ReactNode} props.toolbar The toolbar containing the editors commands.
+ *
+ * @example Customizing the editors options
+ * import { RichTextInput, RichTextInputToolbar } from 'ra-input-rich-text';
+ * const MyRichTextInput = (props) => (
+ * }
+ * label="Body"
+ * source="body"
+ * {...props}
+ * />
+ * );
+ *
+ * @example Customizing the toolbar size
+ * import { RichTextInput, RichTextInputToolbar } from 'ra-input-rich-text';
+ * const MyRichTextInput = (props) => (
+ * }
+ * label="Body"
+ * source="body"
+ * {...props}
+ * />
+ * );
+ *
+ * @example Customizing the toolbar commands
+ * import { RichTextInput, RichTextInputToolbar } from 'ra-input-rich-text';
+ * const MyRichTextInput = ({ size, ...props }) => (
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * )}
+ * label="Body"
+ * source="body"
+ * {...props}
+ * />
+ * );
+ */
export const RichTextInput = (props: RichTextInputProps) => {
const {
- options = {}, // Quill editor options
- toolbar = true,
- fullWidth = true,
- configureQuill,
+ defaultValue = '',
+ disabled = false,
+ editorOptions = DefaultEditorOptions,
+ fullWidth,
helperText,
label,
+ readOnly = false,
source,
- resource,
- variant,
- margin = 'dense',
- ...rest
+ toolbar = ,
} = props;
- const quillInstance = useRef();
- const divRef = useRef();
- const editor = useRef();
+ const resource = useResourceContext(props);
const {
id,
+ field,
isRequired,
- field: { value, onChange },
- fieldState: { invalid, isTouched, error },
+ fieldState,
formState: { isSubmitted },
- } = useInput({ source, ...rest });
-
- const lastValueChange = useRef(value);
+ } = useInput({ ...props, source, defaultValue });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const onTextChange = useCallback(
- debounce(() => {
- const value =
- editor.current.innerHTML === '
'
- ? ''
- : editor.current.innerHTML;
+ const editor = useEditor({
+ ...editorOptions,
+ content: field.value,
+ editorProps: {
+ attributes: {
+ id,
+ ...(disabled || readOnly
+ ? EditorAttributesNotEditable
+ : EditorAttributes),
+ },
+ },
+ });
- if (lastValueChange.current !== value) {
- lastValueChange.current = value;
- onChange(value);
- }
- }, 500),
- [onChange]
- );
+ const { error, invalid, isTouched } = fieldState;
useEffect(() => {
- quillInstance.current = new Quill(divRef.current, {
- modules: { toolbar, clipboard: { matchVisual: false } },
- theme: 'snow',
- ...options,
+ if (!editor) return;
+
+ editor.setOptions({
+ editorProps: {
+ attributes: {
+ id,
+ ...(disabled || readOnly
+ ? EditorAttributesNotEditable
+ : EditorAttributes),
+ },
+ },
});
+ }, [disabled, editor, readOnly, id]);
- if (configureQuill) {
- configureQuill(quillInstance.current);
+ useEffect(() => {
+ if (!editor) {
+ return;
}
- quillInstance.current.setContents(
- quillInstance.current.clipboard.convert(value)
- );
+ const handleEditorUpdate = () => {
+ if (editor.isEmpty) {
+ field.onChange('');
+ field.onBlur();
+ return;
+ }
- editor.current = divRef.current.querySelector('.ql-editor');
- quillInstance.current.on('text-change', onTextChange);
+ const html = editor.getHTML();
+ field.onChange(html);
+ field.onBlur();
+ };
+ editor.on('update', handleEditorUpdate);
return () => {
- quillInstance.current.off('text-change', onTextChange);
- if (onTextChange.cancel) {
- onTextChange.cancel();
- }
- quillInstance.current = null;
+ editor.off('update', handleEditorUpdate);
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- useEffect(() => {
- if (lastValueChange.current !== value) {
- const selection = quillInstance.current.getSelection();
- quillInstance.current.setContents(
- quillInstance.current.clipboard.convert(value)
- );
- if (selection && quillInstance.current.hasFocus()) {
- quillInstance.current.setSelection(selection);
- }
- }
- }, [value]);
+ }, [editor, field]);
return (
-
- {/* @ts-ignore */}
-
-
-
-
-
-
-
-
-
+
+
);
};
-export interface RichTextInputProps {
- debounce?: number;
- label?: string | false;
- options?: QuillOptionsStatic;
- source: string;
- toolbar?:
- | boolean
- | string[]
- | Array[]
- | string
- | {
- container: string | string[] | Array[];
- handlers?: Record;
- };
- fullWidth?: boolean;
- configureQuill?: (instance: Quill) => void;
- helperText?: ComponentProps['helperText'];
- record?: Record;
- resource?: string;
- variant?: string;
- margin?: 'normal' | 'none' | 'dense';
- [key: string]: any;
-}
-
-RichTextInput.propTypes = {
- // @ts-ignore
- label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
- options: PropTypes.object,
- source: PropTypes.string,
- fullWidth: PropTypes.bool,
- configureQuill: PropTypes.func,
+/**
+ * Extracted in a separate component so that we can remove fullWidth from the props injected by Labeled
+ * and avoid warnings about unknown props on Root.
+ */
+const RichTextInputContent = ({
+ editor,
+ error,
+ fullWidth,
+ helperText,
+ id,
+ isTouched,
+ isSubmitted,
+ invalid,
+ toolbar,
+}: RichTextInputContentProps) => (
+
+
+ {toolbar}
+
+
+
+
+
+
+);
+
+const EditorAttributes = {
+ role: 'textbox',
+ 'aria-multiline': 'true',
};
-const StyledFormControl = styled(FormControl)(RaRichTextStyles);
+const EditorAttributesNotEditable = {
+ role: 'textbox',
+ 'aria-multiline': 'true',
+ contenteditable: false,
+ 'aria-readonly': 'true',
+};
+
+export const DefaultEditorOptions = {
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ TextAlign.configure({
+ types: ['heading', 'paragraph'],
+ }),
+ ],
+};
+
+const PREFIX = 'RaRichTextInput';
+const classes = {
+ root: `${PREFIX}-root`,
+ editorContent: `${PREFIX}-editorContent`,
+};
+const Root = styled('div')(({ theme }) => ({
+ [`&.${classes.root}`]: {
+ display: 'flex',
+ alignItems: 'center',
+ backgroundColor: theme.palette.primary.main,
+ },
+ [`& .${classes.editorContent}`]: {
+ '& > div': {
+ padding: theme.spacing(1),
+ borderStyle: 'solid',
+ borderWidth: '1px',
+ borderColor:
+ theme.palette.mode === 'light'
+ ? 'rgba(0, 0, 0, 0.23)'
+ : 'rgba(255, 255, 255, 0.23)',
+ borderRadius: theme.shape.borderRadius,
+ },
+ },
+}));
+
+export type RichTextInputProps = CommonInputProps &
+ Omit & {
+ editorOptions?: Partial;
+ toolbar?: ReactNode;
+ };
+
+export type RichTextInputContentProps = {
+ editor?: Editor;
+ error?: any;
+ fullWidth?: boolean;
+ helperText?: string | ReactElement | false;
+ id: string;
+ isTouched: boolean;
+ isSubmitted: boolean;
+ invalid: boolean;
+ toolbar?: ReactNode;
+};
diff --git a/packages/ra-input-rich-text/src/RichTextInputLevelSelect.tsx b/packages/ra-input-rich-text/src/RichTextInputLevelSelect.tsx
new file mode 100644
index 00000000000..98658ce12d7
--- /dev/null
+++ b/packages/ra-input-rich-text/src/RichTextInputLevelSelect.tsx
@@ -0,0 +1,205 @@
+import * as React from 'react';
+import { useState } from 'react';
+import { List, ListItem, ListItemText, Menu, MenuItem } from '@mui/material';
+import { styled, alpha } from '@mui/material/styles';
+import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
+import { Editor } from '@tiptap/react';
+import { useTranslate } from 'ra-core';
+import clsx from 'clsx';
+import { useTiptapEditor } from './useTiptapEditor';
+
+export const RichTextInputLevelSelect = (
+ props: RichTextInputLevelSelectProps
+) => {
+ const translate = useTranslate();
+ const editor = useTiptapEditor();
+ const [anchorElement, setAnchorElement] = useState(
+ null
+ );
+ const { size } = props;
+
+ const handleMenuItemClick = (
+ event: React.MouseEvent,
+ index: number
+ ) => {
+ setAnchorElement(null);
+ const selectedItem = options[index];
+ if (selectedItem.value === 'paragraph') {
+ editor.chain().focus().setParagraph().run();
+ } else if (selectedItem.value === 'heading') {
+ editor
+ .chain()
+ .focus()
+ .setHeading({ level: selectedItem.level })
+ .run();
+ }
+ };
+
+ const handleClickListItem = (
+ event: React.MouseEvent
+ ) => {
+ setAnchorElement(event.currentTarget);
+ };
+
+ const handleClose = (event: React.MouseEvent) => {
+ setAnchorElement(null);
+ };
+
+ const selectedOption =
+ options.find(option => isSelectedOption(editor, option)) || options[0];
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+type LevelOption = ParagraphLevelOption | HeadingLevelOption;
+
+type ParagraphLevelOption = {
+ label: string;
+ defaultLabel: string;
+ value: 'paragraph';
+};
+
+type HeadingLevelOption = {
+ label: string;
+ defaultLabel: string;
+ value: 'heading';
+ level: 1 | 2 | 3 | 4 | 5 | 6;
+};
+
+const options: Array = [
+ {
+ label: 'ra.tiptap.paragraph',
+ defaultLabel: 'Normal',
+ value: 'paragraph',
+ },
+ {
+ label: 'ra.tiptap.heading1',
+ defaultLabel: 'Heading 1',
+ value: 'heading',
+ level: 1,
+ },
+ {
+ label: 'ra.tiptap.heading2',
+ defaultLabel: 'Heading 2',
+ value: 'heading',
+ level: 2,
+ },
+ {
+ label: 'ra.tiptap.heading3',
+ defaultLabel: 'Heading 3',
+ value: 'heading',
+ level: 3,
+ },
+ {
+ label: 'ra.tiptap.heading4',
+ defaultLabel: 'Heading 4',
+ value: 'heading',
+ level: 4,
+ },
+ {
+ label: 'ra.tiptap.heading5',
+ defaultLabel: 'Heading 5',
+ value: 'heading',
+ level: 5,
+ },
+ {
+ label: 'ra.tiptap.heading6',
+ defaultLabel: 'Heading 6',
+ value: 'heading',
+ level: 6,
+ },
+];
+
+const isSelectedOption = (editor: Editor, option: LevelOption): boolean => {
+ if (!editor) {
+ return false;
+ }
+
+ if (option.value === 'paragraph') {
+ return editor.isActive('paragraph');
+ }
+
+ return editor.isActive('heading', { level: option.level });
+};
+
+const PREFIX = 'RaRichTextInputLevelSelect';
+const classes = {
+ list: `${PREFIX}-list`,
+ sizeSmall: `${PREFIX}-sizeSmall`,
+ sizeLarge: `${PREFIX}-sizeLarge`,
+};
+const Root = styled('div')(({ theme }) => ({
+ [`&.${classes.list}`]: {
+ borderRadius: theme.shape.borderRadius,
+ border: `1px solid ${alpha(theme.palette.action.active, 0.12)}`,
+ },
+ [`& .${classes.sizeSmall}`]: {
+ paddingTop: 1,
+ paddingBottom: 1,
+ '& .MuiTypography-root': {
+ fontSize: theme.typography.pxToRem(13),
+ },
+ },
+ [`& .${classes.sizeLarge}`]: {
+ paddingTop: 8,
+ paddingBottom: 8,
+ '& .MuiTypography-root': {
+ fontSize: theme.typography.pxToRem(15),
+ },
+ },
+}));
+
+export type RichTextInputLevelSelectProps = {
+ size?: 'small' | 'medium' | 'large';
+};
diff --git a/packages/ra-input-rich-text/src/RichTextInputToolbar.tsx b/packages/ra-input-rich-text/src/RichTextInputToolbar.tsx
new file mode 100644
index 00000000000..fc84063fc18
--- /dev/null
+++ b/packages/ra-input-rich-text/src/RichTextInputToolbar.tsx
@@ -0,0 +1,96 @@
+import * as React from 'react';
+import { ReactNode } from 'react';
+import { styled } from '@mui/material/styles';
+import { RichTextInputLevelSelect } from './RichTextInputLevelSelect';
+import {
+ AlignmentButtons,
+ FormatButtons,
+ ListButtons,
+ LinkButtons,
+ QuoteButtons,
+ ClearButtons,
+} from './buttons';
+
+/**
+ * A toolbar for the .
+ * @param props The toolbar props.
+ * @param {ReactNode} props.children The toolbar children, usually many .
+ * @param {'small' | 'medium' | 'large'} props.size The default size to apply to the **default** children.
+ *
+ * @example Customizing the size
+ * import { RichTextInput, RichTextInputToolbar } from 'ra-input-rich-text';
+ * const MyRichTextInput = (props) => (
+ * }
+ * label="Body"
+ * source="body"
+ * {...props}
+ * />
+ * );
+ *
+ * @example Customizing the children
+ * import { RichTextInput, RichTextInputToolbar } from 'ra-input-rich-text';
+ * const MyRichTextInput = ({ size, ...props }) => (
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * )}
+ * label="Body"
+ * source="body"
+ * {...props}
+ * />
+ * );
+ */
+export const RichTextInputToolbar = (props: RichTextInputToolbarProps) => {
+ const {
+ size = 'medium',
+ children = (
+ <>
+
+
+
+
+
+
+
+ >
+ ),
+ ...rest
+ } = props;
+
+ return (
+
+ {children}
+
+ );
+};
+
+const PREFIX = 'RaRichTextInputToolbar';
+const classes = {
+ root: `${PREFIX}-root`,
+};
+const Root = styled('div')(({ theme }) => ({
+ [`&.${classes.root}`]: {
+ display: 'inline-flex',
+ marginBottom: theme.spacing(1),
+ alignItems: 'center',
+ '& > *': {
+ marginRight: theme.spacing(1),
+ },
+ '& > *:last-child': {
+ marginRight: 0,
+ },
+ },
+}));
+
+export type RichTextInputToolbarProps = {
+ children?: ReactNode;
+ size?: 'small' | 'medium' | 'large';
+};
diff --git a/packages/ra-input-rich-text/src/TiptapEditorContext.tsx b/packages/ra-input-rich-text/src/TiptapEditorContext.tsx
new file mode 100644
index 00000000000..ce98b9db8b1
--- /dev/null
+++ b/packages/ra-input-rich-text/src/TiptapEditorContext.tsx
@@ -0,0 +1,4 @@
+import { createContext } from 'react';
+import { Editor } from '@tiptap/react';
+
+export const TiptapEditorContext = createContext(undefined);
diff --git a/packages/ra-input-rich-text/src/TiptapEditorProvider.tsx b/packages/ra-input-rich-text/src/TiptapEditorProvider.tsx
new file mode 100644
index 00000000000..8d3998ec74d
--- /dev/null
+++ b/packages/ra-input-rich-text/src/TiptapEditorProvider.tsx
@@ -0,0 +1,17 @@
+import * as React from 'react';
+import { Editor } from '@tiptap/react';
+import { TiptapEditorContext } from './TiptapEditorContext';
+
+export const TiptapEditorProvider = ({
+ children,
+ value,
+}: TiptapEditorProviderProps) => (
+
+ {children}
+
+);
+
+export type TiptapEditorProviderProps = {
+ children: React.ReactNode;
+ value: Editor;
+};
diff --git a/packages/ra-input-rich-text/src/buttons/AlignmentButtons.tsx b/packages/ra-input-rich-text/src/buttons/AlignmentButtons.tsx
new file mode 100644
index 00000000000..1a5f80b2aba
--- /dev/null
+++ b/packages/ra-input-rich-text/src/buttons/AlignmentButtons.tsx
@@ -0,0 +1,86 @@
+import * as React from 'react';
+import { MouseEvent } from 'react';
+
+import { Editor } from '@tiptap/react';
+import {
+ ToggleButton,
+ ToggleButtonGroup,
+ ToggleButtonGroupProps,
+} from '@mui/material';
+import FormatAlignCenter from '@mui/icons-material/FormatAlignCenter';
+import FormatAlignLeft from '@mui/icons-material/FormatAlignLeft';
+import FormatAlignRight from '@mui/icons-material/FormatAlignRight';
+import FormatAlignJustify from '@mui/icons-material/FormatAlignJustify';
+
+import { useTranslate } from 'ra-core';
+import { useTiptapEditor } from '../useTiptapEditor';
+
+export const AlignmentButtons = (props: ToggleButtonGroupProps) => {
+ const editor = useTiptapEditor();
+ const translate = useTranslate();
+
+ const leftLabel = translate('ra.tiptap.align_left', { _: 'Align left' });
+ const rightLabel = translate('ra.tiptap.align_right', { _: 'Align right' });
+ const centerLabel = translate('ra.tiptap.align_center', { _: 'Center' });
+ const justifyLabel = translate('ra.tiptap.align_justify', { _: 'Justify' });
+
+ const handleChange = (
+ event: MouseEvent,
+ newFormat: string
+ ) => {
+ AlignmentActions[newFormat](editor);
+ };
+
+ const value = AlignmentValues.reduce((acc, value) => {
+ if (editor && editor.isActive({ textAlign: value })) {
+ return value;
+ }
+ return acc;
+ }, '');
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const AlignmentValues = ['left', 'center', 'right', 'justify', 'code'];
+
+const AlignmentActions = {
+ left: (editor: Editor) => editor.chain().focus().setTextAlign('left').run(),
+ center: (editor: Editor) =>
+ editor.chain().focus().setTextAlign('center').run(),
+ right: (editor: Editor) =>
+ editor.chain().focus().setTextAlign('right').run(),
+ justify: (editor: Editor) =>
+ editor.chain().focus().setTextAlign('justify').run(),
+};
diff --git a/packages/ra-input-rich-text/src/buttons/ClearButtons.tsx b/packages/ra-input-rich-text/src/buttons/ClearButtons.tsx
new file mode 100644
index 00000000000..e16355d5a48
--- /dev/null
+++ b/packages/ra-input-rich-text/src/buttons/ClearButtons.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react';
+import { ToggleButton, ToggleButtonGroupProps } from '@mui/material';
+import FormatClear from '@mui/icons-material/FormatClear';
+import { useTranslate } from 'ra-core';
+import { useTiptapEditor } from '../useTiptapEditor';
+
+export const ClearButtons = (props: ToggleButtonGroupProps) => {
+ const editor = useTiptapEditor();
+ const translate = useTranslate();
+
+ const label = translate('ra.tiptap.clear_format', {
+ _: 'Clear format',
+ });
+
+ return (
+ editor.chain().focus().unsetAllMarks().run()}
+ selected={false}
+ >
+
+
+ );
+};
diff --git a/packages/ra-input-rich-text/src/buttons/FormatButtons.tsx b/packages/ra-input-rich-text/src/buttons/FormatButtons.tsx
new file mode 100644
index 00000000000..a9b3a987e81
--- /dev/null
+++ b/packages/ra-input-rich-text/src/buttons/FormatButtons.tsx
@@ -0,0 +1,124 @@
+import * as React from 'react';
+import { MouseEvent } from 'react';
+
+import { Editor } from '@tiptap/react';
+import {
+ ToggleButton,
+ ToggleButtonGroup,
+ ToggleButtonGroupProps,
+} from '@mui/material';
+import FormatBold from '@mui/icons-material/FormatBold';
+import FormatItalic from '@mui/icons-material/FormatItalic';
+import FormatUnderlined from '@mui/icons-material/FormatUnderlined';
+import FormatStrikethrough from '@mui/icons-material/FormatStrikethrough';
+import Code from '@mui/icons-material/Code';
+import { useTranslate } from 'ra-core';
+import { useTiptapEditor } from '../useTiptapEditor';
+
+export const FormatButtons = (props: ToggleButtonGroupProps) => {
+ const editor = useTiptapEditor();
+ const translate = useTranslate();
+
+ const boldLabel = translate('ra.tiptap.bold', {
+ _: 'Bold',
+ });
+
+ const italicLabel = translate('ra.tiptap.Italic', {
+ _: 'Italic',
+ });
+
+ const underlineLabel = translate('ra.tiptap.underline', {
+ _: 'Underline',
+ });
+
+ const strikeLabel = translate('ra.tiptap.strike', {
+ _: 'Strikethrough',
+ });
+
+ const codeLabel = translate('ra.tiptap.code', {
+ _: 'Code',
+ });
+
+ const handleChange = (
+ event: MouseEvent,
+ newFormats: string[]
+ ) => {
+ FormatValues.forEach(format => {
+ const shouldBeDeactivated =
+ editor &&
+ editor.isActive(format) &&
+ !newFormats.includes(format);
+ const shouldBeActivated =
+ editor &&
+ !editor.isActive(format) &&
+ newFormats.includes(format);
+
+ if (shouldBeDeactivated || shouldBeActivated) {
+ FormatActions[format](editor);
+ }
+ });
+ };
+
+ const value = FormatValues.reduce((acc, value) => {
+ if (editor && editor.isActive(value)) {
+ acc.push(value);
+ }
+ return acc;
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const FormatValues = ['bold', 'italic', 'underline', 'strike', 'code'];
+
+const FormatActions = {
+ bold: (editor: Editor) => editor.chain().focus().toggleBold().run(),
+ italic: (editor: Editor) => editor.chain().focus().toggleItalic().run(),
+ underline: (editor: Editor) =>
+ editor.chain().focus().toggleUnderline().run(),
+ strike: (editor: Editor) => editor.chain().focus().toggleStrike().run(),
+ code: (editor: Editor) => editor.chain().focus().toggleCode().run(),
+};
diff --git a/packages/ra-input-rich-text/src/buttons/LinkButtons.tsx b/packages/ra-input-rich-text/src/buttons/LinkButtons.tsx
new file mode 100644
index 00000000000..8a3a43df600
--- /dev/null
+++ b/packages/ra-input-rich-text/src/buttons/LinkButtons.tsx
@@ -0,0 +1,47 @@
+import * as React from 'react';
+import { ToggleButton, ToggleButtonGroupProps } from '@mui/material';
+import InsertLink from '@mui/icons-material/InsertLink';
+
+import { useTranslate } from 'ra-core';
+import { useTiptapEditor } from '../useTiptapEditor';
+
+export const LinkButtons = (props: ToggleButtonGroupProps) => {
+ const editor = useTiptapEditor();
+ const translate = useTranslate();
+ const disabled = editor
+ ? editor.state.doc.textBetween(
+ editor.state.selection.from,
+ editor.state.selection.to
+ ).length === 0
+ : false;
+
+ const label = translate('ra.tiptap.link', {
+ _: 'Add a link',
+ });
+
+ return (
+ {
+ if (!editor.can().setLink({ href: '' })) {
+ return;
+ }
+
+ const url = window.prompt('URL');
+
+ editor
+ .chain()
+ .focus()
+ .extendMarkRange('link')
+ .setLink({ href: url })
+ .run();
+ }}
+ selected={editor && editor.isActive('link')}
+ disabled={disabled}
+ >
+
+
+ );
+};
diff --git a/packages/ra-input-rich-text/src/buttons/ListButtons.tsx b/packages/ra-input-rich-text/src/buttons/ListButtons.tsx
new file mode 100644
index 00000000000..c5a2863cece
--- /dev/null
+++ b/packages/ra-input-rich-text/src/buttons/ListButtons.tsx
@@ -0,0 +1,81 @@
+import * as React from 'react';
+import { MouseEvent } from 'react';
+
+import { Editor } from '@tiptap/react';
+import {
+ ToggleButton,
+ ToggleButtonGroup,
+ ToggleButtonGroupProps,
+} from '@mui/material';
+import FormatListBulleted from '@mui/icons-material/FormatListBulleted';
+import FormatListNumbered from '@mui/icons-material/FormatListNumbered';
+
+import { useTranslate } from 'ra-core';
+import { useTiptapEditor } from '../useTiptapEditor';
+
+export const ListButtons = (props: ToggleButtonGroupProps) => {
+ const editor = useTiptapEditor();
+ const translate = useTranslate();
+
+ const bulletListLabel = translate('ra.tiptap.list_bulleted', {
+ _: 'Bulleted list',
+ });
+ const numberListLabel = translate('ra.tiptap.list_numbered', {
+ _: 'Numbered list',
+ });
+
+ const handleChange = (
+ event: MouseEvent,
+ newFormat: string
+ ) => {
+ ListValues.forEach(format => {
+ const shouldBeDeactivated =
+ editor && editor.isActive(format) && newFormat !== format;
+ const shouldBeActivated =
+ editor && !editor.isActive(format) && newFormat === format;
+
+ if (shouldBeDeactivated || shouldBeActivated) {
+ ListActions[format](editor);
+ }
+ });
+ };
+
+ const value = ListValues.reduce((acc, value) => {
+ if (editor && editor.isActive(value)) {
+ return value;
+ }
+ return acc;
+ }, '');
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+const ListValues = ['bulletList', 'orderedList'];
+const ListActions = {
+ bulletList: (editor: Editor) =>
+ editor.chain().focus().toggleBulletList().run(),
+ orderedList: (editor: Editor) =>
+ editor.chain().focus().toggleOrderedList().run(),
+};
diff --git a/packages/ra-input-rich-text/src/buttons/QuoteButtons.tsx b/packages/ra-input-rich-text/src/buttons/QuoteButtons.tsx
new file mode 100644
index 00000000000..763fded4ebb
--- /dev/null
+++ b/packages/ra-input-rich-text/src/buttons/QuoteButtons.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react';
+import { ToggleButton, ToggleButtonGroupProps } from '@mui/material';
+import FormatQuote from '@mui/icons-material/FormatQuote';
+import { useTranslate } from 'ra-core';
+import { useTiptapEditor } from '../useTiptapEditor';
+
+export const QuoteButtons = (props: ToggleButtonGroupProps) => {
+ const editor = useTiptapEditor();
+ const translate = useTranslate();
+
+ const label = translate('ra.tiptap.blockquote', {
+ _: 'Blockquote',
+ });
+
+ return (
+ editor.chain().focus().toggleBlockquote().run()}
+ selected={editor && editor.isActive('blockquote')}
+ >
+
+
+ );
+};
diff --git a/packages/ra-input-rich-text/src/buttons/index.ts b/packages/ra-input-rich-text/src/buttons/index.ts
new file mode 100644
index 00000000000..38e7f0d0cce
--- /dev/null
+++ b/packages/ra-input-rich-text/src/buttons/index.ts
@@ -0,0 +1,6 @@
+export * from './FormatButtons';
+export * from './ListButtons';
+export * from './AlignmentButtons';
+export * from './LinkButtons';
+export * from './QuoteButtons';
+export * from './ClearButtons';
diff --git a/packages/ra-input-rich-text/src/index.ts b/packages/ra-input-rich-text/src/index.ts
index 87131999d46..ed538e84623 100644
--- a/packages/ra-input-rich-text/src/index.ts
+++ b/packages/ra-input-rich-text/src/index.ts
@@ -1 +1,7 @@
export * from './RichTextInput';
+export * from './buttons';
+export * from './RichTextInputLevelSelect';
+export * from './RichTextInputToolbar';
+export * from './TiptapEditorContext';
+export * from './TiptapEditorProvider';
+export * from './useTiptapEditor';
diff --git a/packages/ra-input-rich-text/src/styles.ts b/packages/ra-input-rich-text/src/styles.ts
deleted file mode 100644
index db8a73d33dd..00000000000
--- a/packages/ra-input-rich-text/src/styles.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import { Theme } from '@mui/material/styles';
-
-const PREFIX = 'RaRichTextInput';
-
-export const RaRichTextClasses = {
- root: `${PREFIX}-root`,
- label: `${PREFIX}-label`,
-};
-
-export const RaRichTextStyles = ({ theme }: { theme?: Theme }) => ({
- label: {
- position: 'relative',
- },
- root: {
- '& .ql-editor': {
- fontSize: '1rem',
- fontFamily: 'Roboto, sans-serif',
- padding: '6px 12px',
- backgroundColor:
- theme.palette.mode === 'dark'
- ? 'rgba(255, 255, 255, 0.04)'
- : 'rgba(0, 0, 0, 0.04)',
- '&:hover::before': {
- backgroundColor:
- theme.palette.mode === 'dark'
- ? 'rgba(255, 255, 255, 1)'
- : 'rgba(0, 0, 0, 1)',
- height: 2,
- },
-
- '&::before': {
- left: 0,
- right: 0,
- bottom: 0,
- height: 1,
- content: '""',
- position: 'absolute',
- transition:
- 'background-color 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
- backgroundColor:
- theme.palette.mode === 'dark'
- ? 'rgba(255, 255, 255, 0.7)'
- : 'rgba(0, 0, 0, 0.5)',
- },
-
- '&::after': {
- left: 0,
- right: 0,
- bottom: 0,
- height: 2,
- content: '""',
- position: 'absolute',
- transform: 'scaleX(0)',
- transition: 'transform 200ms cubic-bezier(0, 0, 0.2, 1) 0ms',
- backgroundColor: theme.palette.primary.main,
- },
-
- '& p:not(:last-child)': {
- marginBottom: '1rem',
- },
-
- '& strong': {
- fontWeight: 700,
- },
- '& h1': {
- margin: '1rem 0 0.5rem 0',
- fontWeight: 500,
- },
- '& h2': {
- margin: '1rem 0 0.5rem 0',
- fontWeight: 500,
- },
- '& h3': {
- margin: '1rem 0 0.5rem 0',
- fontWeight: 500,
- },
- '& a': {
- color: theme.palette.primary.main,
- },
- '& ul': {
- marginBottom: '1rem',
- },
-
- '& li:not(.ql-direction-rtl)::before': {
- fontSize: '0.5rem',
- position: 'relative',
- top: '-0.2rem',
- marginRight: '0.5rem',
- },
-
- '&:focus::after': {
- transform: 'scaleX(1)',
- },
- },
- '& .standard .ql-editor': {
- backgroundColor: theme.palette.background.paper,
- },
- '& .outlined .ql-editor': {
- backgroundColor: theme.palette.background.paper,
- },
- '& .ql-toolbar.ql-snow': {
- margin: '0.5rem 0',
- border: 0,
- padding: 0,
-
- '& .ql-picker-item': {
- color: theme.palette.text.primary,
- },
- '& .ql-stroke': {
- stroke: theme.palette.text.primary,
- },
- '& .ql-fill': {
- fill: theme.palette.text.primary,
- },
- '& .ql-picker-item.ql-active': {
- color: theme.palette.primary.main,
- },
- '& .ql-picker-item:hover': {
- color: theme.palette.primary.main,
- },
- '& .ql-picker-item.ql-selected': {
- color: theme.palette.primary.main,
- },
- '& .ql-picker-label.ql-active': {
- color: theme.palette.primary.main,
- },
- '& .ql-picker-label.ql-selected': {
- color: theme.palette.primary.main,
- },
- '& .ql-picker-label:hover': {
- color: theme.palette.primary.main,
- },
-
- '& button:hover .ql-fill': {
- fill: theme.palette.primary.main,
- },
- '& button.ql-active .ql-fill': {
- fill: theme.palette.primary.main,
- },
-
- '& button:hover .ql-stroke': {
- stroke: theme.palette.primary.main,
- },
- '& button.ql-active .ql-stroke': {
- stroke: theme.palette.primary.main,
- },
- '& .ql-picker-label:hover .ql-stroke': {
- stroke: theme.palette.primary.main,
- },
-
- '& .ql-picker.ql-expanded .ql-picker-options': {
- backgroundColor: theme.palette.background.paper,
- borderColor: theme.palette.background.paper,
- },
-
- '& .ql-snow .ql-picker.ql-expanded .ql-picker-options': {
- background: '#fff',
- zIndex: 10,
- },
-
- '& .ql-picker-label': {
- paddingLeft: 0,
- color: theme.palette.text.primary,
- },
-
- '& + .ql-container.ql-snow': {
- border: 0,
- },
- },
- },
-});
diff --git a/packages/ra-input-rich-text/src/useTiptapEditor.ts b/packages/ra-input-rich-text/src/useTiptapEditor.ts
new file mode 100644
index 00000000000..c95e5919bdb
--- /dev/null
+++ b/packages/ra-input-rich-text/src/useTiptapEditor.ts
@@ -0,0 +1,8 @@
+import { useContext } from 'react';
+import { TiptapEditorContext } from './TiptapEditorContext';
+
+export const useTiptapEditor = () => {
+ const context = useContext(TiptapEditorContext);
+
+ return context;
+};
diff --git a/packages/ra-input-rich-text/tsconfig.json b/packages/ra-input-rich-text/tsconfig.json
index 04f238f9657..fe5802d2571 100644
--- a/packages/ra-input-rich-text/tsconfig.json
+++ b/packages/ra-input-rich-text/tsconfig.json
@@ -1,11 +1,12 @@
{
- "extends": "../../tsconfig.json",
- "compilerOptions": {
- "outDir": "lib",
- "rootDir": "src",
- "declaration": true,
- "declarationMap": true,
- },
- "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"],
- "include": ["src"]
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "lib",
+ "rootDir": "src",
+ "declaration": true,
+ "declarationMap": true,
+ "allowJs": false
+ },
+ "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"],
+ "include": ["src"]
}
diff --git a/packages/ra-ui-materialui/src/defaultTheme.ts b/packages/ra-ui-materialui/src/defaultTheme.ts
index 1a7b2ab49f9..9deab6a8c35 100644
--- a/packages/ra-ui-materialui/src/defaultTheme.ts
+++ b/packages/ra-ui-materialui/src/defaultTheme.ts
@@ -45,6 +45,23 @@ export const defaultTheme = {
opacity: 0.3,
borderRadius: 'inherit',
},
+ '&:focus::after': {
+ // This ensures we provide visual cues to users using the keyboard
+ // recreate a static ripple color
+ // use the currentColor to make it work both for outlined and contained buttons
+ // but to dim the background without dimming the text,
+ // put another element on top with a limited opacity
+ content: '""',
+ display: 'block',
+ width: '100%',
+ height: '100%',
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ backgroundColor: 'currentColor',
+ opacity: 0.3,
+ borderRadius: 'inherit',
+ },
},
},
},
diff --git a/packages/ra-ui-materialui/src/input/Labeled.tsx b/packages/ra-ui-materialui/src/input/Labeled.tsx
index a35a8032838..20b3869a703 100644
--- a/packages/ra-ui-materialui/src/input/Labeled.tsx
+++ b/packages/ra-ui-materialui/src/input/Labeled.tsx
@@ -30,6 +30,7 @@ export const Labeled = (props: LabeledProps) => {
field,
isRequired,
label,
+ labelId,
margin = 'dense',
fieldState,
source,
@@ -55,7 +56,12 @@ export const Labeled = (props: LabeledProps) => {
error={fieldState && fieldState.isTouched && !!fieldState.error}
margin={margin}
>
-
+