From d30eec40a8bf80d963877ceccd2b1054b65d32ee Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 6 Jul 2022 08:04:29 +0100 Subject: [PATCH] Update to Draftail v2 alpha --- CHANGELOG.txt | 6 ++ .../CommentableEditor.test.tsx | 12 ++-- .../CommentableEditor/CommentableEditor.tsx | 34 ++++------ client/src/components/Draftail/Draftail.scss | 2 +- .../Draftail/__snapshots__/index.test.js.snap | 8 +++ .../__snapshots__/MediaBlock.test.js.snap | 2 - .../__snapshots__/TooltipEntity.test.js.snap | 2 - .../src/entrypoints/admin/telepath/widgets.js | 12 ++-- .../admin/telepath/widgets.test.js | 4 +- docs/releases/4.0.md | 11 ++++ package-lock.json | 65 ++++++++++++------- package.json | 2 +- wagtail/admin/wagtail_hooks.py | 2 +- wagtail/images/wagtail_hooks.py | 2 +- 14 files changed, 98 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9161d07d15..68f78d4e50 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -58,6 +58,12 @@ Changelog * Migrate the dashboard (home) view header to the shared header template and update designs (Paarth Agarwal) * Update classes and styles for the shared header templates to align with UI guidelines (Paarth Agarwal) * Clean up multiple eslint rules usage and configs to align better with the Wagtail coding guidelines (LB (Ben Johnston)) + * Add a live-updating character count to the Draftail rich text editor (Thibaud Colas) + * Add rich text editor paste to auto-create links (Thibaud Colas) + * Add rich text editor text shortcuts undo, to allow typing text normally detected as a shortcut (Thibaud Colas) + * Add support for right-to-left (RTL) languages to the rich text editor (Thibaud Colas) + * Change rich text editor placeholder to follow the user’s focus on empty blocks (Thibaud Colas) + * Add rich text editor empty block highlight by showing their block type (Thibaud Colas) * Fix: Typo in `ResumeWorkflowActionFormatter` message (Stefan Hammer) * Fix: Throw a meaningful error when saving an image to an unrecognised image format (Christian Franke) * Fix: Remove extra padding for headers with breadcrumbs on mobile viewport (Steven Steinwand) diff --git a/client/src/components/Draftail/CommentableEditor/CommentableEditor.test.tsx b/client/src/components/Draftail/CommentableEditor/CommentableEditor.test.tsx index f33b30ec5f..15ae588968 100644 --- a/client/src/components/Draftail/CommentableEditor/CommentableEditor.test.tsx +++ b/client/src/components/Draftail/CommentableEditor/CommentableEditor.test.tsx @@ -2,7 +2,7 @@ import React, { ReactNode } from 'react'; import { Provider } from 'react-redux'; import { mount } from 'enzyme'; import { createEditorStateFromRaw } from 'draftail'; -import { EditorState, SelectionState } from 'draft-js'; +import { DraftInlineStyleType, EditorState, SelectionState } from 'draft-js'; import { CommentApp } from '../../CommentApp/main'; import { updateGlobalSettings } from '../../CommentApp/actions/settings'; @@ -42,7 +42,7 @@ describe('CommentableEditor', () => { { offset: 0, length: 1, - style: 'COMMENT-1', + style: 'COMMENT-1' as DraftInlineStyleType, }, ], text: 'test', @@ -61,12 +61,12 @@ describe('CommentableEditor', () => { { offset: 0, length: 10, - style: 'COMMENT-2', + style: 'COMMENT-2' as DraftInlineStyleType, }, { offset: 0, length: 20, - style: 'COMMENT-1', + style: 'COMMENT-1' as DraftInlineStyleType, }, ], text: 'test_test_test_test_test_test_test', @@ -85,12 +85,12 @@ describe('CommentableEditor', () => { { offset: 21, length: 4, - style: 'COMMENT-2', + style: 'COMMENT-2' as DraftInlineStyleType, }, { offset: 0, length: 20, - style: 'COMMENT-1', + style: 'COMMENT-1' as DraftInlineStyleType, }, ], text: 'test_test_test_test_test_test_test', diff --git a/client/src/components/Draftail/CommentableEditor/CommentableEditor.tsx b/client/src/components/Draftail/CommentableEditor/CommentableEditor.tsx index 4f464d8b0b..db5093849c 100644 --- a/client/src/components/Draftail/CommentableEditor/CommentableEditor.tsx +++ b/client/src/components/Draftail/CommentableEditor/CommentableEditor.tsx @@ -3,6 +3,9 @@ import { ToolbarButton, createEditorStateFromRaw, serialiseEditorStateToRaw, + InlineStyleControl, + ControlComponentProps, + DraftailEditorProps, } from 'draftail'; import { CharacterMetadata, @@ -21,7 +24,6 @@ import { filterInlineStyles } from 'draftjs-filters'; import React, { MutableRefObject, ReactNode, - ReactText, useEffect, useMemo, useRef, @@ -266,12 +268,6 @@ function createFromBlockArrayOrPlaceholder(blockArray: ContentBlock[]) { return ContentState.createFromText(' '); } -interface ControlProps { - getEditorState: () => EditorState; - // eslint-disable-next-line react/no-unused-prop-types - onChange: (editorState: EditorState) => void; -} - export function splitState(editorState: EditorState) { const selection = editorState.getSelection(); const anchorKey = selection.getAnchorKey(); @@ -345,7 +341,7 @@ export function getSplitControl( ); } - return ({ getEditorState }: ControlProps) => ( + return ({ getEditorState }: ControlComponentProps) => ( ( + return ({ getEditorState, onChange }: ControlComponentProps) => ( ; -} - interface ColorConfigProp { standardHighlight: string; overlappingHighlight: string; @@ -685,14 +673,14 @@ interface CommentableEditorProps { fieldNode: Element; contentPath: string; rawContentState: RawDraftContentState; - onSave: (rawContent: RawDraftContentState) => void; - inlineStyles: Array; + onSave: (rawContent: RawDraftContentState | null) => void; + inlineStyles: InlineStyleControl[]; editorRef: (editor: ReactNode) => void; colorConfig: ColorConfigProp; isCommentShortcut: (e: React.KeyboardEvent) => boolean; // Unfortunately the EditorPlugin type isn't exported in our version of 'draft-js-plugins-editor' plugins?: Record[]; - controls?: Array<(props: ControlProps) => JSX.Element>; + controls?: DraftailEditorProps['controls']; } function CommentableEditor({ @@ -733,7 +721,7 @@ function CommentableEditor({ [comments], ); - const commentStyles: Array = useMemo( + const commentStyles: InlineStyleControl[] = useMemo( () => ids.map((id) => ({ type: `${COMMENT_STYLE_IDENTIFIER}${id}`, @@ -863,7 +851,9 @@ function CommentableEditor({ setEditorState(newEditorState); }} editorState={editorState} - controls={enabled ? controls.concat([CommentControl]) : controls} + controls={ + enabled ? controls.concat([{ block: CommentControl }]) : controls + } inlineStyles={inlineStyles.concat(commentStyles)} plugins={plugins.concat([ { diff --git a/client/src/components/Draftail/Draftail.scss b/client/src/components/Draftail/Draftail.scss index d9adec08ae..203206ad90 100644 --- a/client/src/components/Draftail/Draftail.scss +++ b/client/src/components/Draftail/Draftail.scss @@ -13,7 +13,7 @@ $draftail-toolbar-icon-size: 1em; $draftail-editor-font-family: $font-sans; @import '../../../../node_modules/draft-js/dist/Draft'; -@import '../../../../node_modules/draftail/lib/index'; +@import '../../../../node_modules/draftail/src/index'; @import './Tooltip/Tooltip'; @import './CommentableEditor/CommentableEditor'; diff --git a/client/src/components/Draftail/__snapshots__/index.test.js.snap b/client/src/components/Draftail/__snapshots__/index.test.js.snap index 7a0afb9c8b..7c5c1a31da 100644 --- a/client/src/components/Draftail/__snapshots__/index.test.js.snap +++ b/client/src/components/Draftail/__snapshots__/index.test.js.snap @@ -3,11 +3,18 @@ exports[`Draftail #initEditor options 1`] = ` Object { "ariaDescribedBy": null, + "ariaExpanded": null, + "ariaLabel": null, + "ariaLabelledBy": null, + "ariaOwneeID": null, + "ariaRequired": null, "autoCapitalize": null, "autoComplete": null, "autoCorrect": null, "blockTypes": Array [], "bottomToolbar": null, + "commandToolbar": [Function], + "commands": false, "controls": Array [], "decorators": Array [], "editorState": null, @@ -27,6 +34,7 @@ Object { ], "inlineStyles": Array [], "maxListNesting": 4, + "multiline": true, "onBlur": null, "onChange": null, "onFocus": null, diff --git a/client/src/components/Draftail/blocks/__snapshots__/MediaBlock.test.js.snap b/client/src/components/Draftail/blocks/__snapshots__/MediaBlock.test.js.snap index 0abc1214b4..2f96e73df0 100644 --- a/client/src/components/Draftail/blocks/__snapshots__/MediaBlock.test.js.snap +++ b/client/src/components/Draftail/blocks/__snapshots__/MediaBlock.test.js.snap @@ -15,7 +15,6 @@ exports[`MediaBlock no data 1`] = ` test test diff --git a/client/src/entrypoints/admin/telepath/widgets.js b/client/src/entrypoints/admin/telepath/widgets.js index b5f06c3e07..7cce2a8389 100644 --- a/client/src/entrypoints/admin/telepath/widgets.js +++ b/client/src/entrypoints/admin/telepath/widgets.js @@ -241,11 +241,13 @@ class DraftailRichTextArea { ...currentOptions, controls: [ ...(originalOptions || []), - // eslint-disable-next-line no-undef - draftail.getSplitControl( - newCapability.fn, - !!newCapability.enabled, - ), + { + // eslint-disable-next-line no-undef + block: draftail.getSplitControl( + newCapability.fn, + !!newCapability.enabled, + ), + }, ], }); } diff --git a/client/src/entrypoints/admin/telepath/widgets.test.js b/client/src/entrypoints/admin/telepath/widgets.test.js index dd4ef42441..8f3df1bf96 100644 --- a/client/src/entrypoints/admin/telepath/widgets.test.js +++ b/client/src/entrypoints/admin/telepath/widgets.test.js @@ -416,7 +416,7 @@ describe('telepath: wagtail.widgets.DraftailRichTextArea', () => { icon: 'link', description: 'Link', attributes: ['url', 'id', 'parentId'], - whitelist: { + allowlist: { href: '^(http:|https:|undefined$)', }, }, @@ -427,7 +427,7 @@ describe('telepath: wagtail.widgets.DraftailRichTextArea', () => { icon: 'image', description: 'Image', attributes: ['id', 'src', 'alt', 'format'], - whitelist: { + allowlist: { id: true, }, }, diff --git a/docs/releases/4.0.md b/docs/releases/4.0.md index 11e2680f39..ffd7065984 100644 --- a/docs/releases/4.0.md +++ b/docs/releases/4.0.md @@ -13,6 +13,17 @@ depth: 1 When using a queryset to render a list of images, you can now use the `prefetch_renditions()` queryset method to prefetch the renditions needed for rendering with a single extra query, similar to `prefetch_related`. If you have many renditions per image, you can also call it with filters as arguments - `prefetch_renditions("fill-700x586", "min-600x400")` - to fetch only the renditions you intend on using for a smaller query. For long lists of images, this can provide a significant boost to performance. See [](prefetching_image_renditions) for more examples. This feature was developed by Tidiane Dia and Karl Hobley. +### Rich text improvements + +As part of the page editor redesign project sponsored by Google, we have made a number of improvements to our rich text editor: + +* Character count: The character count is displayed underneath the editor, live-updating as you type. This counts the length of the text, not of any formatting. +* Paste to auto-create links: To add a link from your copy-paste clipboard, select text and paste the URL. +* Text shortcuts undo: The editor normally converts text starting with `1. ` to a list item. It’s now possible to un-do this change and keep the text as-is. This works for all Markdown-style shortcuts. +* RTL support: The editor’s UI now displays correctly in right-to-left languages. +* Focus-aware placeholder: The editor’s placeholder text will now follow the user’s focus, to make it easier to understand where to type in long fields. +* Empty heading highlight: The editor now highlights empty headings and list items by showing their type (“Heading 3”) as a placeholder, so content is less likely to be published with empty headings. + ### Other features * Add clarity to confirmation when being asked to convert an external link to an internal one (Thijs Kramer) diff --git a/package-lock.json b/package-lock.json index 70bb12f712..1c32fbc255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@tippyjs/react": "^4.2.6", "a11y-dialog": "^7.4.0", "draft-js": "^0.10.5", - "draftail": "^1.4.1", + "draftail": "^2.0.0-alpha.1", "draftjs-filters": "^2.5.0", "focus-trap-react": "^8.4.2", "immer": "^9.0.6", @@ -16420,7 +16420,6 @@ }, "node_modules/compute-scroll-into-view": { "version": "1.0.17", - "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -17831,7 +17830,6 @@ }, "node_modules/downshift": { "version": "6.1.7", - "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.14.8", @@ -17874,26 +17872,37 @@ } }, "node_modules/draftail": { - "version": "1.4.1", - "license": "MIT", + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/draftail/-/draftail-2.0.0-alpha.1.tgz", + "integrity": "sha512-D6IEHS3KBFgnPIy4Et+O2SU8hbpZ+jdLyVCVXD7uifavK5c4ZFQ/w/zkVw7/8HJoMTyUIHNNSOHmdbCPnogQRg==", "dependencies": { + "@tippyjs/react": "^4.2.6", "decorate-component-with-props": "^1.0.2", + "downshift": "^6.1.7", "draft-js-plugins-editor": "^2.1.1", - "draftjs-conductor": "^1.0.0", - "draftjs-filters": "^2.2.3" + "draftjs-conductor": "^3.0.0", + "draftjs-filters": "^3.0.1" }, "peerDependencies": { "draft-js": "^0.10.5", - "react": "^16.0.0", - "react-dom": "^16.0.0" + "react": "^16.6.0", + "react-dom": "^16.6.0" + } + }, + "node_modules/draftail/node_modules/draftjs-filters": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/draftjs-filters/-/draftjs-filters-3.0.1.tgz", + "integrity": "sha512-sms/ACV6w/dc/mRkmrBw6o0+lxE8SevRg+5k31Y/FJIYf+ALVKGltcVixEwh5XSYVgddjHkMJoC4hmx1m6Fcxg==", + "peerDependencies": { + "draft-js": "^0.10.4 || ^0.11.0 || ^0.12.0" } }, "node_modules/draftjs-conductor": { - "version": "1.2.0", - "license": "MIT", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/draftjs-conductor/-/draftjs-conductor-3.0.0.tgz", + "integrity": "sha512-OjFRwOT41IVKUvdOC/jnLl9CYMP8mVbonVh5MS9Ud2LGOhllWFFFqz90uc9vUAh3DPMHe5iMFGSqXUSW6a5PFw==", "peerDependencies": { - "draft-js": "^0.10.5 || ^0.11.0", - "react": "^16.0.0" + "draft-js": "^0.10.4 || ^0.11.0 || ^0.12.0" } }, "node_modules/draftjs-filters": { @@ -29932,7 +29941,6 @@ }, "node_modules/tslib": { "version": "2.3.1", - "dev": true, "license": "0BSD" }, "node_modules/tsutils": { @@ -42796,8 +42804,7 @@ } }, "compute-scroll-into-view": { - "version": "1.0.17", - "dev": true + "version": "1.0.17" }, "concat-map": { "version": "0.0.1", @@ -43767,7 +43774,6 @@ }, "downshift": { "version": "6.1.7", - "dev": true, "requires": { "@babel/runtime": "^7.14.8", "compute-scroll-into-view": "^1.0.17", @@ -43795,16 +43801,30 @@ } }, "draftail": { - "version": "1.4.1", + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/draftail/-/draftail-2.0.0-alpha.1.tgz", + "integrity": "sha512-D6IEHS3KBFgnPIy4Et+O2SU8hbpZ+jdLyVCVXD7uifavK5c4ZFQ/w/zkVw7/8HJoMTyUIHNNSOHmdbCPnogQRg==", "requires": { + "@tippyjs/react": "^4.2.6", "decorate-component-with-props": "^1.0.2", + "downshift": "^6.1.7", "draft-js-plugins-editor": "^2.1.1", - "draftjs-conductor": "^1.0.0", - "draftjs-filters": "^2.2.3" + "draftjs-conductor": "^3.0.0", + "draftjs-filters": "^3.0.1" + }, + "dependencies": { + "draftjs-filters": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/draftjs-filters/-/draftjs-filters-3.0.1.tgz", + "integrity": "sha512-sms/ACV6w/dc/mRkmrBw6o0+lxE8SevRg+5k31Y/FJIYf+ALVKGltcVixEwh5XSYVgddjHkMJoC4hmx1m6Fcxg==", + "requires": {} + } } }, "draftjs-conductor": { - "version": "1.2.0", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/draftjs-conductor/-/draftjs-conductor-3.0.0.tgz", + "integrity": "sha512-OjFRwOT41IVKUvdOC/jnLl9CYMP8mVbonVh5MS9Ud2LGOhllWFFFqz90uc9vUAh3DPMHe5iMFGSqXUSW6a5PFw==", "requires": {} }, "draftjs-filters": { @@ -51673,8 +51693,7 @@ } }, "tslib": { - "version": "2.3.1", - "dev": true + "version": "2.3.1" }, "tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 258581aa3e..5e3b2b2952 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "@tippyjs/react": "^4.2.6", "a11y-dialog": "^7.4.0", "draft-js": "^0.10.5", - "draftail": "^1.4.1", + "draftail": "^2.0.0-alpha.1", "draftjs-filters": "^2.5.0", "focus-trap-react": "^8.4.2", "immer": "^9.0.6", diff --git a/wagtail/admin/wagtail_hooks.py b/wagtail/admin/wagtail_hooks.py index bd74ac3fbc..6ac23f854e 100644 --- a/wagtail/admin/wagtail_hooks.py +++ b/wagtail/admin/wagtail_hooks.py @@ -733,7 +733,7 @@ def register_core_features(features): # We want to enforce constraints on which links can be pasted into rich text. # Keep only the attributes Wagtail needs. "attributes": ["url", "id", "parentId"], - "whitelist": { + "allowlist": { # Keep pasted links with http/https protocol, and not-pasted links (href = undefined). "href": "^(http:|https:|undefined$)", }, diff --git a/wagtail/images/wagtail_hooks.py b/wagtail/images/wagtail_hooks.py index fa618d1c12..98ce84aea4 100644 --- a/wagtail/images/wagtail_hooks.py +++ b/wagtail/images/wagtail_hooks.py @@ -94,7 +94,7 @@ def register_image_feature(features): # Keep only the attributes Wagtail needs. "attributes": ["id", "src", "alt", "format"], # Keep only images which are from Wagtail. - "whitelist": { + "allowlist": { "id": True, }, },