Add RichTextEditor component using native block editor toolbar (#34865)
* Add initial rich text editor component * Use fixed toolbar and add base formatters * Add link as formatter option * Fix up references to core/link block * Add quote transform * Add changelog entry * Add text alignment toolbar * Remove references to checkbox list * Update toolbar button organization * Remove unused rtj format types * Create unique instance for editor writing flow * Add registry provider for storybook examples * Update styling for editor * Rebase and fix lock file * Add packages and type dependencies * Move component to experimental * Fix up formatting * Update editor to use default GB toolbar * Prefix names with woocoommerce * Create block on initialization so toolbar is visible * Rely on insertBlock to handle selection * Update text editor to use setting instead of BlockList prop * Fix up lock file after rebase * Handle PR feedback * Move logic for force rerender * Fix up pnpm lock file * Use trunk lock file * Update lock file * Add missing semicolon * Use Pauls lock file and package file
This commit is contained in:
parent
0ca3f6d589
commit
618cc54a3f
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add rich text editor component
|
|
@ -33,6 +33,10 @@
|
|||
"@automattic/calypso-color-schemes": "^2.1.1",
|
||||
"@automattic/interpolate-components": "^1.2.0",
|
||||
"@automattic/tour-kit": "^1.1.1",
|
||||
"@types/wordpress__block-editor": "^7.0.0",
|
||||
"@types/wordpress__block-library": "^2.6.1",
|
||||
"@types/wordpress__blocks": "^11.0.7",
|
||||
"@types/wordpress__rich-text": "^3.4.6",
|
||||
"@woocommerce/csv-export": "workspace:*",
|
||||
"@woocommerce/currency": "workspace:*",
|
||||
"@woocommerce/data": "workspace:*",
|
||||
|
@ -40,8 +44,12 @@
|
|||
"@woocommerce/navigation": "workspace:*",
|
||||
"@wordpress/a11y": "3.5.0",
|
||||
"@wordpress/api-fetch": "^6.0.1",
|
||||
"@wordpress/block-editor": "^9.8.0",
|
||||
"@wordpress/block-library": "^7.16.0",
|
||||
"@wordpress/blocks": "^11.18.0",
|
||||
"@wordpress/components": "^19.5.0",
|
||||
"@wordpress/compose": "^5.1.2",
|
||||
"@wordpress/core-data": "^4.2.1",
|
||||
"@wordpress/date": "^4.3.1",
|
||||
"@wordpress/deprecated": "^3.3.1",
|
||||
"@wordpress/dom": "^3.3.2",
|
||||
|
@ -50,8 +58,10 @@
|
|||
"@wordpress/html-entities": "^3.3.1",
|
||||
"@wordpress/i18n": "^4.3.1",
|
||||
"@wordpress/icons": "^8.1.0",
|
||||
"@wordpress/keyboard-shortcuts": "^3.17.0",
|
||||
"@wordpress/keycodes": "^3.3.1",
|
||||
"@wordpress/media-utils": "^4.6.0",
|
||||
"@wordpress/rich-text": "^5.17.0",
|
||||
"@wordpress/url": "^3.4.1",
|
||||
"@wordpress/viewport": "^4.1.2",
|
||||
"classnames": "^2.3.1",
|
||||
|
|
|
@ -34,6 +34,7 @@ export { default as ProductRating } from './rating/product';
|
|||
export { default as Rating } from './rating';
|
||||
export { default as ReportFilters } from './filters';
|
||||
export { default as ReviewRating } from './rating/review';
|
||||
export { RichTextEditor as __experimentalRichTextEditor } from './rich-text-editor';
|
||||
export { default as Search } from './search';
|
||||
export { default as SearchListControl } from './search-list-control';
|
||||
export { default as SearchListItem } from './search-list-control/item';
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { createElement, useEffect } from '@wordpress/element';
|
||||
import { createBlock } from '@wordpress/blocks';
|
||||
import {
|
||||
BlockList,
|
||||
ObserveTyping,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
BlockTools,
|
||||
store as blockEditorStore,
|
||||
WritingFlow,
|
||||
} from '@wordpress/block-editor';
|
||||
|
||||
export const EditorWritingFlow: React.VFC = () => {
|
||||
const instanceId = useInstanceId( EditorWritingFlow );
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore This action is available in the block editor data store.
|
||||
const { insertBlock } = useDispatch( blockEditorStore );
|
||||
|
||||
const { isEmpty } = useSelect( ( select ) => {
|
||||
const blocks = select( 'core/block-editor' ).getBlocks();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore This selector is available in the block editor data store.
|
||||
const { getSelectedBlockClientIds } = select( blockEditorStore );
|
||||
|
||||
return {
|
||||
isEmpty: blocks.length
|
||||
? blocks.length <= 1 &&
|
||||
blocks[ 0 ].attributes?.content?.trim() === ''
|
||||
: true,
|
||||
firstBlock: blocks[ 0 ],
|
||||
selectedBlockClientIds: getSelectedBlockClientIds(),
|
||||
};
|
||||
} );
|
||||
|
||||
useEffect( () => {
|
||||
if ( isEmpty ) {
|
||||
const initialBlock = createBlock( 'core/paragraph', {
|
||||
content: '',
|
||||
} );
|
||||
insertBlock( initialBlock );
|
||||
}
|
||||
}, [] );
|
||||
|
||||
return (
|
||||
/* Gutenberg handles the keyboard events when focusing the content editable area. */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||
<div
|
||||
className="woocommerce-rich-text-editor__writing-flow"
|
||||
id={ `woocommerce-rich-text-editor__writing-flow-${ instanceId }` }
|
||||
style={ {
|
||||
cursor: isEmpty ? 'text' : 'initial',
|
||||
} }
|
||||
>
|
||||
<BlockTools>
|
||||
<WritingFlow>
|
||||
<ObserveTyping>
|
||||
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
||||
{ /* @ts-ignore This action is available in the block editor data store. */ }
|
||||
<BlockList />
|
||||
</ObserveTyping>
|
||||
</WritingFlow>
|
||||
</BlockTools>
|
||||
</div>
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './rich-text-editor';
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockEditorProvider } from '@wordpress/block-editor';
|
||||
import { BlockInstance } from '@wordpress/blocks';
|
||||
import { SlotFillProvider } from '@wordpress/components';
|
||||
import { debounce } from 'lodash';
|
||||
import {
|
||||
createElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
} from '@wordpress/element';
|
||||
import React from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
// eslint-disable-next-line @woocommerce/dependency-group
|
||||
import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { EditorWritingFlow } from './editor-writing-flow';
|
||||
import { registerBlocks } from './utils/register-blocks';
|
||||
|
||||
registerBlocks();
|
||||
|
||||
type RichTextEditorProps = {
|
||||
blocks: BlockInstance[];
|
||||
onChange: ( changes: BlockInstance[] ) => void;
|
||||
entryId?: string;
|
||||
};
|
||||
|
||||
export const RichTextEditor: React.VFC< RichTextEditorProps > = ( {
|
||||
blocks,
|
||||
onChange,
|
||||
} ) => {
|
||||
const blocksRef = useRef( blocks );
|
||||
|
||||
const [ , setRefresh ] = useState( 0 );
|
||||
|
||||
// If there is a props change we need to update the ref and force re-render.
|
||||
// Note: Because this component is memoized and because we don't re-render
|
||||
// when this component initiates a change, a prop change won't force the re-render
|
||||
// you'd expect. A change to the blocks must come from outside the editor.
|
||||
const forceRerender = () => {
|
||||
setRefresh( ( refresh ) => refresh + 1 );
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
blocksRef.current = blocks;
|
||||
forceRerender();
|
||||
}, [ blocks ] );
|
||||
|
||||
const debounceChange = debounce( ( updatedBlocks ) => {
|
||||
onChange( updatedBlocks );
|
||||
blocksRef.current = updatedBlocks;
|
||||
forceRerender();
|
||||
}, 200 );
|
||||
|
||||
return (
|
||||
<div className="woocommerce-rich-text-editor">
|
||||
<SlotFillProvider>
|
||||
<BlockEditorProvider
|
||||
value={ blocksRef.current }
|
||||
settings={ {
|
||||
bodyPlaceholder: '',
|
||||
hasFixedToolbar: true,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore This property was recently added in the block editor data store.
|
||||
__experimentalClearBlockSelection: false,
|
||||
} }
|
||||
onInput={ debounceChange }
|
||||
onChange={ debounceChange }
|
||||
>
|
||||
<ShortcutProvider>
|
||||
<EditorWritingFlow />
|
||||
</ShortcutProvider>
|
||||
</BlockEditorProvider>
|
||||
</SlotFillProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import React from 'react';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { createRegistry, RegistryProvider } from '@wordpress/data';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
// eslint-disable-next-line @woocommerce/dependency-group
|
||||
import { store as coreDataStore } from '@wordpress/core-data';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
// eslint-disable-next-line @woocommerce/dependency-group
|
||||
import { store as blockEditorStore } from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { RichTextEditor } from '../';
|
||||
|
||||
const registry = createRegistry();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
registry.register( coreDataStore );
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
registry.register( blockEditorStore );
|
||||
|
||||
export const Basic: React.FC = () => {
|
||||
return (
|
||||
<RegistryProvider value={ registry }>
|
||||
<RichTextEditor blocks={ [] } onChange={ () => null } />
|
||||
</RegistryProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const MultipleEditors: React.FC = () => {
|
||||
return (
|
||||
<RegistryProvider value={ registry }>
|
||||
<RichTextEditor blocks={ [] } onChange={ () => null } />
|
||||
<br />
|
||||
<RichTextEditor blocks={ [] } onChange={ () => null } />
|
||||
</RegistryProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'WooCommerce Admin/experimental/RichTextEditor',
|
||||
component: RichTextEditor,
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
.woocommerce-rich-text-editor {
|
||||
border: 1px solid $gray-600;
|
||||
border-radius: 2px;
|
||||
background: $white;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.block-editor-inserter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.block-editor-block-contextual-toolbar.is-fixed,
|
||||
.block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar-group,
|
||||
.block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar {
|
||||
border-color: $gray-600;
|
||||
}
|
||||
|
||||
/* Hide rich text placeholder text */
|
||||
.rich-text [data-rich-text-placeholder] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* hide block boundary background styling */
|
||||
.rich-text:focus *[data-rich-text-format-boundary] {
|
||||
background: none !important;
|
||||
}
|
||||
.block-editor-block-list__layout
|
||||
.block-editor-block-list__block.is-multi-selected::after {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
.block-editor-block-list__layout
|
||||
.block-editor-block-list__block:not([contenteditable]):focus::after {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.block-editor-block-list__empty-block-inserter,
|
||||
.block-editor-block-list__insertion-point {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.block-editor-writing-flow {
|
||||
padding: $gap-small;
|
||||
}
|
||||
|
||||
/* Within the writing flow reset some of the css resets used elsewhere */
|
||||
.block-editor-writing-flow ul,
|
||||
.block-editor-writing-flow ol {
|
||||
list-style: revert;
|
||||
padding: revert;
|
||||
}
|
||||
|
||||
.components-accessible-toolbar {
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
border-color: $gray-700;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.wp-block-quote {
|
||||
border-left: 0.25em solid currentColor;
|
||||
margin: 0 0 1.75em 0;
|
||||
padding-left: 1em;
|
||||
|
||||
cite {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockInstance } from '@wordpress/blocks';
|
||||
import {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore We need this to import the block modules for registration.
|
||||
__experimentalGetCoreBlocks,
|
||||
registerCoreBlocks as wpRegisterCoreBlocks,
|
||||
} from '@wordpress/block-library';
|
||||
|
||||
export const PARAGRAPH_BLOCK_ID = 'core/paragraph';
|
||||
export const HEADING_BLOCK_ID = 'core/heading';
|
||||
export const LIST_BLOCK_ID = 'core/list';
|
||||
export const LIST_ITEM_BLOCK_ID = 'core/list-item';
|
||||
export const QUOTE_BLOCK_ID = 'core/quote';
|
||||
|
||||
const ALLOWED_CORE_BLOCKS = [
|
||||
PARAGRAPH_BLOCK_ID,
|
||||
HEADING_BLOCK_ID,
|
||||
LIST_BLOCK_ID,
|
||||
LIST_ITEM_BLOCK_ID,
|
||||
QUOTE_BLOCK_ID,
|
||||
];
|
||||
|
||||
const registerCoreBlocks = () => {
|
||||
const coreBlocks = __experimentalGetCoreBlocks();
|
||||
const blocks = coreBlocks.filter( ( block: BlockInstance ) =>
|
||||
ALLOWED_CORE_BLOCKS.includes( block.name )
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore An argument is allowed to specify which blocks to register.
|
||||
wpRegisterCoreBlocks( blocks );
|
||||
};
|
||||
|
||||
export const registerBlocks = () => {
|
||||
registerCoreBlocks();
|
||||
};
|
|
@ -32,6 +32,7 @@
|
|||
@import 'pill/style.scss';
|
||||
@import 'product-image/style.scss';
|
||||
@import 'rating/style.scss';
|
||||
@import 'rich-text-editor/style.scss';
|
||||
@import 'search/style.scss';
|
||||
@import 'search-list-control/style.scss';
|
||||
@import 'section-header/style.scss';
|
||||
|
|
1216
pnpm-lock.yaml
1216
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue