Fix up rich text editor initial selection and add blocks (#35286)
* Fix double click toolbar behavior * Fix initial block selection on editor load * Add placeholder option to RichTextEditor * Add image and video blocks * Set toolbar height * Allow inserter to be shown * Allow media uploads in rich text editor * Add changelog entries * Fix media upload * Check for existence of selected blocks before checking length * Pass blocks to avoid race in detecting initially empty blocks
This commit is contained in:
parent
66370c823f
commit
e1ebabba29
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: tweak
|
||||
|
||||
Fix up initial block selection in RichTextEditor and add media blocks
|
|
@ -3,8 +3,8 @@
|
|||
*/
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { BlockInstance, createBlock } from '@wordpress/blocks';
|
||||
import { createElement, useEffect } from '@wordpress/element';
|
||||
import { createBlock } from '@wordpress/blocks';
|
||||
import {
|
||||
BlockList,
|
||||
ObserveTyping,
|
||||
|
@ -15,37 +15,50 @@ import {
|
|||
WritingFlow,
|
||||
} from '@wordpress/block-editor';
|
||||
|
||||
export const EditorWritingFlow: React.VFC = () => {
|
||||
type EditorWritingFlowProps = {
|
||||
blocks: BlockInstance[];
|
||||
onChange: ( changes: BlockInstance[] ) => void;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const EditorWritingFlow = ( {
|
||||
blocks,
|
||||
onChange,
|
||||
placeholder = '',
|
||||
}: EditorWritingFlowProps ) => {
|
||||
const instanceId = useInstanceId( EditorWritingFlow );
|
||||
const firstBlock = blocks[ 0 ];
|
||||
const isEmpty = ! blocks.length;
|
||||
// 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();
|
||||
|
||||
const { insertBlock, selectBlock } = useDispatch( blockEditorStore );
|
||||
const { selectedBlockClientIds } = useSelect( ( select ) => {
|
||||
// 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 ( selectedBlockClientIds?.length || ! firstBlock ) {
|
||||
return;
|
||||
}
|
||||
selectBlock( firstBlock.clientId );
|
||||
}, [ firstBlock, selectedBlockClientIds ] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( isEmpty ) {
|
||||
const initialBlock = createBlock( 'core/paragraph', {
|
||||
content: '',
|
||||
placeholder,
|
||||
} );
|
||||
insertBlock( initialBlock );
|
||||
onChange( [ initialBlock ] );
|
||||
}
|
||||
}, [] );
|
||||
}, [ isEmpty ] );
|
||||
|
||||
return (
|
||||
/* Gutenberg handles the keyboard events when focusing the content editable area. */
|
||||
|
@ -60,8 +73,6 @@ export const EditorWritingFlow: React.VFC = () => {
|
|||
<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>
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BaseControl, SlotFillProvider } from '@wordpress/components';
|
||||
import { BaseControl, Popover, SlotFillProvider } from '@wordpress/components';
|
||||
import { BlockEditorProvider } from '@wordpress/block-editor';
|
||||
import { BlockInstance } from '@wordpress/blocks';
|
||||
import { createElement, useEffect, useState, useRef } from '@wordpress/element';
|
||||
import { debounce } from 'lodash';
|
||||
import {
|
||||
createElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
} from '@wordpress/element';
|
||||
import React from 'react';
|
||||
import { uploadMedia } from '@wordpress/media-utils';
|
||||
import { useUser } from '@woocommerce/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
|
||||
|
@ -31,15 +27,17 @@ type RichTextEditorProps = {
|
|||
label?: string;
|
||||
onChange: ( changes: BlockInstance[] ) => void;
|
||||
entryId?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const RichTextEditor: React.VFC< RichTextEditorProps > = ( {
|
||||
blocks,
|
||||
label,
|
||||
onChange,
|
||||
placeholder = '',
|
||||
} ) => {
|
||||
const blocksRef = useRef( blocks );
|
||||
|
||||
const { currentUserCan } = useUser();
|
||||
const [ , setRefresh ] = useState( 0 );
|
||||
|
||||
// If there is a props change we need to update the ref and force re-render.
|
||||
|
@ -61,6 +59,24 @@ export const RichTextEditor: React.VFC< RichTextEditorProps > = ( {
|
|||
forceRerender();
|
||||
}, 200 );
|
||||
|
||||
const mediaUpload = currentUserCan( 'upload_files' )
|
||||
? ( {
|
||||
onError,
|
||||
...rest
|
||||
}: {
|
||||
onError: ( message: string ) => void;
|
||||
} ) => {
|
||||
uploadMedia(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore The upload function passes the remaining required props.
|
||||
{
|
||||
onError: ( { message } ) => onError( message ),
|
||||
...rest,
|
||||
}
|
||||
);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="woocommerce-rich-text-editor">
|
||||
{ label && (
|
||||
|
@ -75,13 +91,19 @@ export const RichTextEditor: React.VFC< RichTextEditorProps > = ( {
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore This property was recently added in the block editor data store.
|
||||
__experimentalClearBlockSelection: false,
|
||||
mediaUpload,
|
||||
} }
|
||||
onInput={ debounceChange }
|
||||
onChange={ debounceChange }
|
||||
>
|
||||
<ShortcutProvider>
|
||||
<EditorWritingFlow />
|
||||
<EditorWritingFlow
|
||||
blocks={ blocksRef.current }
|
||||
onChange={ onChange }
|
||||
placeholder={ placeholder }
|
||||
/>
|
||||
</ShortcutProvider>
|
||||
<Popover.Slot />
|
||||
</BlockEditorProvider>
|
||||
</SlotFillProvider>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
$toolbar-height: 40px;
|
||||
|
||||
.woocommerce-rich-text-editor {
|
||||
.woocommerce-rich-text-editor__writing-flow {
|
||||
border: 1px solid $gray-600;
|
||||
|
@ -9,21 +11,12 @@
|
|||
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;
|
||||
|
@ -39,11 +32,6 @@
|
|||
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;
|
||||
}
|
||||
|
@ -56,11 +44,25 @@
|
|||
}
|
||||
|
||||
.components-accessible-toolbar {
|
||||
height: $toolbar-height;
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
border-color: $gray-700;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
.components-button {
|
||||
height: $toolbar-height;
|
||||
}
|
||||
}
|
||||
|
||||
.block-editor-block-mover:not(.is-horizontal) .block-editor-block-mover__move-button-container > * {
|
||||
height: calc( $toolbar-height / 2 );
|
||||
}
|
||||
|
||||
.block-editor-block-contextual-toolbar.is-fixed,
|
||||
.components-toolbar-group {
|
||||
min-height: $toolbar-height;
|
||||
}
|
||||
|
||||
.wp-block-quote {
|
||||
|
|
|
@ -14,6 +14,8 @@ 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';
|
||||
export const IMAGE_BLOCK_ID = 'core/image';
|
||||
export const VIDEO_BLOCK_ID = 'core/video';
|
||||
|
||||
const ALLOWED_CORE_BLOCKS = [
|
||||
PARAGRAPH_BLOCK_ID,
|
||||
|
@ -21,6 +23,8 @@ const ALLOWED_CORE_BLOCKS = [
|
|||
LIST_BLOCK_ID,
|
||||
LIST_ITEM_BLOCK_ID,
|
||||
QUOTE_BLOCK_ID,
|
||||
IMAGE_BLOCK_ID,
|
||||
VIDEO_BLOCK_ID,
|
||||
];
|
||||
|
||||
const registerCoreBlocks = () => {
|
||||
|
|
|
@ -226,6 +226,10 @@ export const ProductDetailsSection: React.FC = () => {
|
|||
setDescriptionBlocks( blocks );
|
||||
setValue( 'description', serialize( blocks ) );
|
||||
} }
|
||||
placeholder={ __(
|
||||
'Describe this product. What makes it unique? What are its most important features?',
|
||||
'woocommerce'
|
||||
) }
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add placeholder to description field
|
Loading…
Reference in New Issue