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/). ![RichTextInput](./img/rich-text-input.gif) @@ -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. - -![`` example](https://marmelab.com/react-admin/img/rich-text-input.png) +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) => ( + +
{}} + render={() => ( + <> + + + + )} + {...props} + /> + +); + +export const Small = (props: Partial) => ( + + {}} + render={() => ( + <> + } + label="Body" + source="body" + /> + + + )} + {...props} + /> + +); + +export const Large = (props: Partial) => ( + + {}} + render={() => ( + <> + } + label="Body" + source="body" + /> + + + )} + {...props} + /> + +); + +export const Validation = (props: Partial) => ( + + {}} + render={() => ( + <> + + + + )} + {...props} + /> + +); 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 ( + + + + + + + + + {options.map((option, index) => ( + { + handleMenuItemClick(event, index); + }} + > + {translate(option.label, { _: option.defaultLabel })} + + ))} + + + ); +}; + +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} > - +