diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/container-blocks.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/container-blocks.test.js.snap index fb3f84595e5e2..cbcbf0402f8c9 100644 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/container-blocks.test.js.snap +++ b/packages/e2e-tests/specs/editor/plugins/__snapshots__/container-blocks.test.js.snap @@ -22,10 +22,11 @@ exports[`InnerBlocks Template Sync Ensures blocks without locking are kept intac

Content…

- -

added paragraph

- -" + +

added paragraph

+ + +" `; exports[`InnerBlocks Template Sync Removes blocks that are not expected by the template if a lock all exists 1`] = ` diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index b7aa8bcd025af..87fc0aa408516 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -12,6 +12,7 @@ import { UnsavedChangesWarning, EditorNotices, EditorKeyboardShortcutsRegister, + EditorKeyboardShortcuts, EditorSnackbars, PostSyncStatusModal, store as editorStore, @@ -203,6 +204,7 @@ function Layout( { styles } ) { + - { isRichEditingEnabled && (

{ __( 'Editing code' ) }

diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 29e6e15da4579..fe14ef0556113 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -6,11 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { - VisualEditorGlobalKeyboardShortcuts, - PostTitle, - store as editorStore, -} from '@wordpress/editor'; +import { PostTitle, store as editorStore } from '@wordpress/editor'; import { WritingFlow, BlockList, @@ -345,7 +341,6 @@ export default function VisualEditor( { styles } ) { 'is-template-mode': isTemplateMode, } ) } > - select( editSiteStore ).isListViewOpened(), - [] - ); - const isBlockInspectorOpen = useSelect( - ( select ) => - select( interfaceStore ).getActiveComplementaryArea( - editSiteStore.name - ) === SIDEBAR_BLOCK, - [] - ); - const { redo, undo } = useDispatch( coreStore ); - const { setIsListViewOpened, switchEditorMode } = - useDispatch( editSiteStore ); - const { enableComplementaryArea, disableComplementaryArea } = - useDispatch( interfaceStore ); - const { setIsSaveViewOpened } = useDispatch( editSiteStore ); - - const { replaceBlocks } = useDispatch( blockEditorStore ); - const { getBlockName, getSelectedBlockClientId, getBlockAttributes } = - useSelect( blockEditorStore ); - - const handleTextLevelShortcut = ( event, level ) => { - event.preventDefault(); - const destinationBlockName = - level === 0 ? 'core/paragraph' : 'core/heading'; - const currentClientId = getSelectedBlockClientId(); - if ( currentClientId === null ) { - return; - } - const blockName = getBlockName( currentClientId ); - if ( blockName !== 'core/paragraph' && blockName !== 'core/heading' ) { - return; - } - const attributes = getBlockAttributes( currentClientId ); - const textAlign = - blockName === 'core/paragraph' ? 'align' : 'textAlign'; - const destinationTextAlign = - destinationBlockName === 'core/paragraph' ? 'align' : 'textAlign'; - - replaceBlocks( - currentClientId, - createBlock( destinationBlockName, { - level, - content: attributes.content, - ...{ [ destinationTextAlign ]: attributes[ textAlign ] }, - } ) - ); - }; - - useShortcut( 'core/edit-site/save', ( event ) => { - event.preventDefault(); - - const dirtyEntityRecords = __experimentalGetDirtyEntityRecords(); - const isDirty = !! dirtyEntityRecords.length; - const isSaving = dirtyEntityRecords.some( ( record ) => - isSavingEntityRecord( record.kind, record.name, record.key ) - ); - - if ( ! isSaving && isDirty ) { - setIsSaveViewOpened( true ); - } - } ); - - useShortcut( 'core/edit-site/undo', ( event ) => { - undo(); - event.preventDefault(); - } ); - - useShortcut( 'core/edit-site/redo', ( event ) => { - redo(); - event.preventDefault(); - } ); - - // Only opens the list view. Other functionality for this shortcut happens in the rendered sidebar. - useShortcut( 'core/edit-site/toggle-list-view', () => { - if ( ! isListViewOpen ) { - return; - } - setIsListViewOpened( true ); - } ); - - useShortcut( 'core/edit-site/toggle-block-settings-sidebar', ( event ) => { - // This shortcut has no known clashes, but use preventDefault to prevent any - // obscure shortcuts from triggering. - event.preventDefault(); - - if ( isBlockInspectorOpen ) { - disableComplementaryArea( STORE_NAME ); - } else { - enableComplementaryArea( STORE_NAME, SIDEBAR_BLOCK ); - } - } ); - - useShortcut( 'core/edit-site/toggle-mode', () => { - switchEditorMode( getEditorMode() === 'visual' ? 'text' : 'visual' ); - } ); - - useShortcut( 'core/edit-site/transform-heading-to-paragraph', ( event ) => - handleTextLevelShortcut( event, 0 ) - ); - - [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { - //the loop is based off on a constant therefore - //the hook will execute the same way every time - //eslint-disable-next-line react-hooks/rules-of-hooks - useShortcut( - `core/edit-site/transform-paragraph-to-heading-${ level }`, - ( event ) => handleTextLevelShortcut( event, level ) - ); - } ); - - return null; -} - -export default KeyboardShortcuts; diff --git a/packages/editor/src/components/global-keyboard-shortcuts/index.js b/packages/editor/src/components/global-keyboard-shortcuts/index.js new file mode 100644 index 0000000000000..4b45fe449123f --- /dev/null +++ b/packages/editor/src/components/global-keyboard-shortcuts/index.js @@ -0,0 +1,49 @@ +/** + * WordPress dependencies + */ +import { useShortcut } from '@wordpress/keyboard-shortcuts'; +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; + +export default function EditorKeyboardShortcuts() { + const { redo, undo, savePost } = useDispatch( editorStore ); + const { isEditedPostDirty, isPostSavingLocked } = useSelect( editorStore ); + + useShortcut( 'core/editor/undo', ( event ) => { + undo(); + event.preventDefault(); + } ); + + useShortcut( 'core/editor/redo', ( event ) => { + redo(); + event.preventDefault(); + } ); + + useShortcut( 'core/editor/save', ( event ) => { + event.preventDefault(); + + /** + * Do not save the post if post saving is locked. + */ + if ( isPostSavingLocked() ) { + return; + } + + // TODO: This should be handled in the `savePost` effect in + // considering `isSaveable`. See note on `isEditedPostSaveable` + // selector about dirtiness and meta-boxes. + // + // See: `isEditedPostSaveable` + if ( ! isEditedPostDirty() ) { + return; + } + + savePost(); + } ); + + return null; +} diff --git a/packages/editor/src/components/global-keyboard-shortcuts/save-shortcut.js b/packages/editor/src/components/global-keyboard-shortcuts/save-shortcut.js deleted file mode 100644 index 1843e2d56876e..0000000000000 --- a/packages/editor/src/components/global-keyboard-shortcuts/save-shortcut.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * WordPress dependencies - */ -import { useShortcut } from '@wordpress/keyboard-shortcuts'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { parse } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { store as editorStore } from '../../store'; - -function SaveShortcut( { resetBlocksOnSave } ) { - const { resetEditorBlocks, savePost } = useDispatch( editorStore ); - const { isEditedPostDirty, getPostEdits, isPostSavingLocked } = - useSelect( editorStore ); - - useShortcut( 'core/editor/save', ( event ) => { - event.preventDefault(); - - /** - * Do not save the post if post saving is locked. - */ - if ( isPostSavingLocked() ) { - return; - } - - // TODO: This should be handled in the `savePost` effect in - // considering `isSaveable`. See note on `isEditedPostSaveable` - // selector about dirtiness and meta-boxes. - // - // See: `isEditedPostSaveable` - if ( ! isEditedPostDirty() ) { - return; - } - - // The text editor requires that editor blocks are updated for a - // save to work correctly. Usually this happens when the textarea - // for the code editors blurs, but the shortcut can be used without - // blurring the textarea. - if ( resetBlocksOnSave ) { - const postEdits = getPostEdits(); - if ( postEdits.content && typeof postEdits.content === 'string' ) { - const blocks = parse( postEdits.content ); - resetEditorBlocks( blocks ); - } - } - - savePost(); - } ); - - return null; -} - -export default SaveShortcut; diff --git a/packages/editor/src/components/global-keyboard-shortcuts/text-editor-shortcuts.js b/packages/editor/src/components/global-keyboard-shortcuts/text-editor-shortcuts.js deleted file mode 100644 index 33eb32000de13..0000000000000 --- a/packages/editor/src/components/global-keyboard-shortcuts/text-editor-shortcuts.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Internal dependencies - */ -import SaveShortcut from './save-shortcut'; - -export default function TextEditorGlobalKeyboardShortcuts() { - return ; -} diff --git a/packages/editor/src/components/global-keyboard-shortcuts/visual-editor-shortcuts.js b/packages/editor/src/components/global-keyboard-shortcuts/visual-editor-shortcuts.js deleted file mode 100644 index 5e5875f275845..0000000000000 --- a/packages/editor/src/components/global-keyboard-shortcuts/visual-editor-shortcuts.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * WordPress dependencies - */ -import { useShortcut } from '@wordpress/keyboard-shortcuts'; -import { useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import SaveShortcut from './save-shortcut'; -import { store as editorStore } from '../../store'; - -function VisualEditorGlobalKeyboardShortcuts() { - const { redo, undo } = useDispatch( editorStore ); - - useShortcut( 'core/editor/undo', ( event ) => { - undo(); - event.preventDefault(); - } ); - - useShortcut( 'core/editor/redo', ( event ) => { - redo(); - event.preventDefault(); - } ); - - return ; -} - -export default VisualEditorGlobalKeyboardShortcuts; diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index aa20f9d6ae9fa..39b562806c109 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -1,3 +1,8 @@ +/** + * Internal dependencies + */ +import EditorKeyboardShortcuts from './global-keyboard-shortcuts'; + // Block Creation Components. export * from './autocompleters'; @@ -5,8 +10,7 @@ export * from './autocompleters'; export { default as AutosaveMonitor } from './autosave-monitor'; export { default as DocumentOutline } from './document-outline'; export { default as DocumentOutlineCheck } from './document-outline/check'; -export { default as VisualEditorGlobalKeyboardShortcuts } from './global-keyboard-shortcuts/visual-editor-shortcuts'; -export { default as TextEditorGlobalKeyboardShortcuts } from './global-keyboard-shortcuts/text-editor-shortcuts'; +export { EditorKeyboardShortcuts }; export { default as EditorKeyboardShortcutsRegister } from './global-keyboard-shortcuts/register-shortcuts'; export { default as EditorHistoryRedo } from './editor-history/redo'; export { default as EditorHistoryUndo } from './editor-history/undo'; @@ -84,3 +88,5 @@ export { default as CharacterCount } from './character-count'; export { default as EditorProvider } from './provider'; export * from './deprecated'; +export const VisualEditorGlobalKeyboardShortcuts = EditorKeyboardShortcuts; +export const TextEditorGlobalKeyboardShortcuts = EditorKeyboardShortcuts; diff --git a/packages/editor/src/components/post-text-editor/index.js b/packages/editor/src/components/post-text-editor/index.js index b771c74605526..c07d72f15c4a9 100644 --- a/packages/editor/src/components/post-text-editor/index.js +++ b/packages/editor/src/components/post-text-editor/index.js @@ -7,8 +7,9 @@ import Textarea from 'react-autosize-textarea'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useEffect, useState, useRef } from '@wordpress/element'; -import { parse } from '@wordpress/blocks'; +import { store as coreStore } from '@wordpress/core-data'; +import { useMemo } from '@wordpress/element'; +import { __unstableSerializeAndClean } from '@wordpress/blocks'; import { useDispatch, useSelect } from '@wordpress/data'; import { useInstanceId } from '@wordpress/compose'; import { VisuallyHidden } from '@wordpress/components'; @@ -19,63 +20,34 @@ import { VisuallyHidden } from '@wordpress/components'; import { store as editorStore } from '../../store'; export default function PostTextEditor() { - const postContent = useSelect( - ( select ) => select( editorStore ).getEditedPostContent(), - [] - ); - - const { editPost, resetEditorBlocks } = useDispatch( editorStore ); - - const [ value, setValue ] = useState( postContent ); - const [ isDirty, setIsDirty ] = useState( false ); const instanceId = useInstanceId( PostTextEditor ); - const valueRef = useRef(); - - if ( ! isDirty && value !== postContent ) { - setValue( postContent ); - } + const { content, blocks, type, id } = useSelect( ( select ) => { + const { getEditedEntityRecord } = select( coreStore ); + const { getCurrentPostType, getCurrentPostId } = select( editorStore ); + const _type = getCurrentPostType(); + const _id = getCurrentPostId(); + const editedRecord = getEditedEntityRecord( 'postType', _type, _id ); - /** - * Handles a textarea change event to notify the onChange prop callback and - * reflect the new value in the component's own state. This marks the start - * of the user's edits, if not already changed, preventing future props - * changes to value from replacing the rendered value. This is expected to - * be followed by a reset to dirty state via `stopEditing`. - * - * @see stopEditing - * - * @param {Event} event Change event. - */ - const onChange = ( event ) => { - const newValue = event.target.value; - editPost( { content: newValue } ); - setValue( newValue ); - setIsDirty( true ); - valueRef.current = newValue; - }; - - /** - * Function called when the user has completed their edits, responsible for - * ensuring that changes, if made, are surfaced to the onPersist prop - * callback and resetting dirty state. - */ - const stopEditing = () => { - if ( isDirty ) { - const blocks = parse( value ); - resetEditorBlocks( blocks ); - setIsDirty( false ); - } - }; - - // Ensure changes aren't lost when component unmounts. - useEffect( () => { - return () => { - if ( valueRef.current ) { - const blocks = parse( valueRef.current ); - resetEditorBlocks( blocks ); - } + return { + content: editedRecord?.content, + blocks: editedRecord?.blocks, + type: _type, + id: _id, }; }, [] ); + const { editEntityRecord } = useDispatch( coreStore ); + // Replicates the logic found in getEditedPostContent(). + const value = useMemo( () => { + if ( content instanceof Function ) { + return content( { blocks } ); + } else if ( blocks ) { + // If we have parsed blocks already, they should be our source of truth. + // Parsing applies block deprecations and legacy block conversions that + // unparsed content will not have. + return __unstableSerializeAndClean( blocks ); + } + return content; + }, [ content, blocks ] ); return ( <> @@ -89,8 +61,13 @@ export default function PostTextEditor() { autoComplete="off" dir="auto" value={ value } - onChange={ onChange } - onBlur={ stopEditing } + onChange={ ( event ) => { + editEntityRecord( 'postType', type, id, { + content: event.target.value, + blocks: undefined, + selection: undefined, + } ); + } } className="editor-post-text-editor" id={ `post-content-${ instanceId }` } placeholder={ __( 'Start writing with text or HTML' ) } diff --git a/packages/editor/src/components/post-text-editor/test/index.js b/packages/editor/src/components/post-text-editor/test/index.js deleted file mode 100644 index acaa7e95600b8..0000000000000 --- a/packages/editor/src/components/post-text-editor/test/index.js +++ /dev/null @@ -1,156 +0,0 @@ -/** - * External dependencies - */ -import { act, render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import PostTextEditor from '../'; - -// "Downgrade" ReactAutosizeTextarea to a regular textarea. Assumes aligned -// props interface. -jest.mock( 'react-autosize-textarea', () => ( props ) => ( -