diff --git a/docs/designers-developers/developers/data/data-core-block-editor.md b/docs/designers-developers/developers/data/data-core-block-editor.md index 1b42d76df3e3b4..6d2b1ce547e6ef 100644 --- a/docs/designers-developers/developers/data/data-core-block-editor.md +++ b/docs/designers-developers/developers/data/data-core-block-editor.md @@ -1016,6 +1016,8 @@ content reflected as an edit in state. _Parameters_ - _blocks_ `Array`: Array of blocks. +- _selectionStart_ `Array`: Selection start object. +- _selectionEnd_ `Array`: Selection end object. _Returns_ diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index e3456d65168dbd..2fa7220da998b8 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -364,6 +364,30 @@ _Returns_ - `Array`: Block list. +# **getEditorSelectionEnd** + +Return the current selection end object. + +_Parameters_ + +- _state_ `Object`: + +_Returns_ + +- `Array`: Selection end object. + +# **getEditorSelectionStart** + +Return the current selection start object. + +_Parameters_ + +- _state_ `Object`: + +_Returns_ + +- `Array`: Selection start object. + # **getEditorSettings** Returns the post editor settings. diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index 33eff22a08892b..ce68432f5db588 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -22,6 +22,8 @@ class BlockEditorProvider extends Component { settings, updateSettings, value, + selectionStart, + selectionEnd, resetBlocks, registry, } = this.props; @@ -49,7 +51,7 @@ class BlockEditorProvider extends Component { // subsequent renders. this.isSyncingOutcomingValue = null; this.isSyncingIncomingValue = value; - resetBlocks( value ); + resetBlocks( value, selectionStart, selectionEnd ); } } @@ -78,6 +80,8 @@ class BlockEditorProvider extends Component { const { getBlocks, + getSelectionStart, + getSelectionEnd, isLastBlockChangePersistent, __unstableIsLastBlockChangeIgnored, } = registry.select( 'core/block-editor' ); @@ -120,10 +124,13 @@ class BlockEditorProvider extends Component { blocks = newBlocks; isPersistent = newIsPersistent; + const selectionStart = getSelectionStart(); + const selectionEnd = getSelectionEnd(); + if ( isPersistent ) { - onChange( blocks ); + onChange( blocks, selectionStart, selectionEnd ); } else { - onInput( blocks ); + onInput( blocks, selectionStart, selectionEnd ); } } } ); diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 4dafaa291fcf31..748890795fee64 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -37,14 +37,18 @@ function* ensureDefaultBlock() { * reset to the specified array of blocks, taking precedence over any other * content reflected as an edit in state. * - * @param {Array} blocks Array of blocks. + * @param {Array} blocks Array of blocks. + * @param {Array} selectionStart Selection start object. + * @param {Array} selectionEnd Selection end object. * * @return {Object} Action object. */ -export function resetBlocks( blocks ) { +export function resetBlocks( blocks, selectionStart, selectionEnd ) { return { type: 'RESET_BLOCKS', blocks, + selectionStart, + selectionEnd, }; } @@ -704,4 +708,3 @@ export function __unstableSaveReusableBlock( id, updatedId ) { export function __unstableMarkLastChangeAsPersistent() { return { type: 'MARK_LAST_CHANGE_AS_PERSISTENT' }; } - diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 97a366e8884d2f..3aec38de03d60f 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -910,15 +910,6 @@ export function isCaretWithinFormattedText( state = false, action ) { return state; } -const BLOCK_SELECTION_EMPTY_OBJECT = {}; -const BLOCK_SELECTION_INITIAL_STATE = { - start: BLOCK_SELECTION_EMPTY_OBJECT, - end: BLOCK_SELECTION_EMPTY_OBJECT, - isMultiSelecting: false, - isEnabled: true, - initialPosition: null, -}; - /** * Reducer returning the block selection's state. * @@ -927,34 +918,18 @@ const BLOCK_SELECTION_INITIAL_STATE = { * * @return {Object} Updated state. */ -export function blockSelection( state = BLOCK_SELECTION_INITIAL_STATE, action ) { +export function blockSelection( state = { start: {}, end: {} }, action ) { switch ( action.type ) { - case 'CLEAR_SELECTED_BLOCK': - return BLOCK_SELECTION_INITIAL_STATE; - case 'START_MULTI_SELECT': - if ( state.isMultiSelecting ) { - return state; + case 'CLEAR_SELECTED_BLOCK': { + if ( state.start.clientId || state.end.clientId ) { + return { start: {}, end: {} }; } - return { - ...state, - isMultiSelecting: true, - initialPosition: null, - }; - case 'STOP_MULTI_SELECT': - if ( ! state.isMultiSelecting ) { - return state; - } + return state; + } - return { - ...state, - isMultiSelecting: false, - initialPosition: null, - }; case 'MULTI_SELECT': return { - ...BLOCK_SELECTION_INITIAL_STATE, - isMultiSelecting: state.isMultiSelecting, start: { clientId: action.start }, end: { clientId: action.end }, }; @@ -967,7 +942,6 @@ export function blockSelection( state = BLOCK_SELECTION_INITIAL_STATE, action ) } return { - ...BLOCK_SELECTION_INITIAL_STATE, initialPosition: action.initialPosition, start: { clientId: action.clientId }, end: { clientId: action.clientId }, @@ -976,7 +950,6 @@ export function blockSelection( state = BLOCK_SELECTION_INITIAL_STATE, action ) case 'INSERT_BLOCKS': { if ( action.updateSelection ) { return { - ...BLOCK_SELECTION_INITIAL_STATE, start: { clientId: action.blocks[ 0 ].clientId }, end: { clientId: action.blocks[ 0 ].clientId }, }; @@ -993,7 +966,7 @@ export function blockSelection( state = BLOCK_SELECTION_INITIAL_STATE, action ) return state; } - return BLOCK_SELECTION_INITIAL_STATE; + return { start: {}, end: {} }; case 'REPLACE_BLOCKS': { if ( action.clientIds.indexOf( state.start.clientId ) === -1 ) { return state; @@ -1003,7 +976,7 @@ export function blockSelection( state = BLOCK_SELECTION_INITIAL_STATE, action ) const blockToSelect = action.blocks[ indexToSelect ]; if ( ! blockToSelect ) { - return BLOCK_SELECTION_INITIAL_STATE; + return { start: {}, end: {} }; } if ( @@ -1014,19 +987,12 @@ export function blockSelection( state = BLOCK_SELECTION_INITIAL_STATE, action ) } return { - ...BLOCK_SELECTION_INITIAL_STATE, start: { clientId: blockToSelect.clientId }, end: { clientId: blockToSelect.clientId }, }; } - case 'TOGGLE_SELECTION': - return { - ...BLOCK_SELECTION_INITIAL_STATE, - isEnabled: action.isSelectionEnabled, - }; case 'SELECTION_CHANGE': return { - ...BLOCK_SELECTION_INITIAL_STATE, start: { clientId: action.clientId, attributeKey: action.attributeKey, @@ -1038,6 +1004,51 @@ export function blockSelection( state = BLOCK_SELECTION_INITIAL_STATE, action ) offset: action.endOffset, }, }; + case 'RESET_BLOCKS': { + const { + selectionStart: start = {}, + selectionEnd: end = {}, + } = action; + + return { start, end }; + } + } + + return state; +} + +/** + * Reducer returning whether the user is multi-selecting. + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function isMultiSelecting( state = false, action ) { + switch ( action.type ) { + case 'START_MULTI_SELECT': + return true; + + case 'STOP_MULTI_SELECT': + return false; + } + + return state; +} + +/** + * Reducer returning whether selection is enabled. + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function isSelectionEnabled( state = true, action ) { + switch ( action.type ) { + case 'TOGGLE_SELECTION': + return action.isSelectionEnabled; } return state; @@ -1228,6 +1239,8 @@ export default combineReducers( { isTyping, isCaretWithinFormattedText, blockSelection, + isMultiSelecting, + isSelectionEnabled, blocksMode, blockListSettings, insertionPoint, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 5e8771ff94935b..8350e7fdbe8eff 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -875,7 +875,7 @@ export function hasMultiSelection( state ) { * @return {boolean} True if multi-selecting, false if not. */ export function isMultiSelecting( state ) { - return state.blockSelection.isMultiSelecting; + return state.isMultiSelecting; } /** @@ -886,7 +886,7 @@ export function isMultiSelecting( state ) { * @return {boolean} True if it should be possible to multi-select blocks, false if multi-selection is disabled. */ export function isSelectionEnabled( state ) { - return state.blockSelection.isEnabled; + return state.isSelectionEnabled; } /** diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index 0626f57fb41291..3cab62cf371502 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -23,6 +23,7 @@ import { isTyping, isCaretWithinFormattedText, blockSelection, + isMultiSelecting, preferences, blocksMode, insertionPoint, @@ -1790,8 +1791,6 @@ describe( 'state', () => { start: { clientId: 'kumquat' }, end: { clientId: 'kumquat' }, initialPosition: -1, - isMultiSelecting: false, - isEnabled: true, } ); } ); @@ -1806,9 +1805,6 @@ describe( 'state', () => { expect( state ).toEqual( { start: { clientId: 'ribs' }, end: { clientId: 'chicken' }, - initialPosition: null, - isMultiSelecting: false, - isEnabled: true, } ); } ); @@ -1823,70 +1819,22 @@ describe( 'state', () => { expect( state ).toEqual( { start: { clientId: 'ribs' }, end: { clientId: 'chicken' }, - initialPosition: null, - isMultiSelecting: true, - isEnabled: true, } ); } ); it( 'should start multi selection', () => { - const original = deepFreeze( { start: { clientId: 'ribs' }, end: { clientId: 'ribs' }, isMultiSelecting: false } ); - const state = blockSelection( original, { + const state = isMultiSelecting( false, { type: 'START_MULTI_SELECT', } ); - expect( state ).toEqual( { - start: { clientId: 'ribs' }, - end: { clientId: 'ribs' }, - initialPosition: null, - isMultiSelecting: true, - } ); - } ); - - it( 'should return same reference if already multi-selecting', () => { - const original = deepFreeze( { start: { clientId: 'ribs' }, end: { clientId: 'ribs' }, isMultiSelecting: true } ); - const state = blockSelection( original, { - type: 'START_MULTI_SELECT', - } ); - - expect( state ).toBe( original ); + expect( state ).toBe( true ); } ); - it( 'should end multi selection with selection', () => { - const original = deepFreeze( { start: { clientId: 'ribs' }, end: { clientId: 'chicken' }, isMultiSelecting: true } ); - const state = blockSelection( original, { + const state = isMultiSelecting( true, { type: 'STOP_MULTI_SELECT', } ); - expect( state ).toEqual( { - start: { clientId: 'ribs' }, - end: { clientId: 'chicken' }, - initialPosition: null, - isMultiSelecting: false, - } ); - } ); - - it( 'should return same reference if already ended multi-selecting', () => { - const original = deepFreeze( { start: { clientId: 'ribs' }, end: { clientId: 'chicken' }, isMultiSelecting: false } ); - const state = blockSelection( original, { - type: 'STOP_MULTI_SELECT', - } ); - - expect( state ).toBe( original ); - } ); - - it( 'should end multi selection without selection', () => { - const original = deepFreeze( { start: { clientId: 'ribs' }, end: { clientId: 'ribs' }, isMultiSelecting: true } ); - const state = blockSelection( original, { - type: 'STOP_MULTI_SELECT', - } ); - - expect( state ).toEqual( { - start: { clientId: 'ribs' }, - end: { clientId: 'ribs' }, - initialPosition: null, - isMultiSelecting: false, - } ); + expect( state ).toBe( false ); } ); it( 'should not update the state if the block is already selected', () => { @@ -1910,9 +1858,6 @@ describe( 'state', () => { expect( state1 ).toEqual( { start: {}, end: {}, - initialPosition: null, - isMultiSelecting: false, - isEnabled: true, } ); } ); @@ -1941,9 +1886,6 @@ describe( 'state', () => { expect( state3 ).toEqual( { start: { clientId: 'ribs' }, end: { clientId: 'ribs' }, - initialPosition: null, - isMultiSelecting: false, - isEnabled: true, } ); } ); @@ -1989,9 +1931,6 @@ describe( 'state', () => { expect( state ).toEqual( { start: { clientId: 'wings' }, end: { clientId: 'wings' }, - initialPosition: null, - isEnabled: true, - isMultiSelecting: false, } ); } ); @@ -2033,9 +1972,6 @@ describe( 'state', () => { expect( state ).toEqual( { start: { clientId: 'wings' }, end: { clientId: 'wings' }, - initialPosition: null, - isMultiSelecting: false, - isEnabled: true, } ); } ); @@ -2050,9 +1986,6 @@ describe( 'state', () => { expect( state ).toEqual( { start: {}, end: {}, - initialPosition: null, - isMultiSelecting: false, - isEnabled: true, } ); } ); @@ -2074,8 +2007,6 @@ describe( 'state', () => { const original = deepFreeze( { start: { clientId: 'chicken' }, end: { clientId: 'chicken' }, - initialPosition: null, - isMultiSelecting: false, } ); const state = blockSelection( original, { type: 'REMOVE_BLOCKS', @@ -2085,9 +2016,6 @@ describe( 'state', () => { expect( state ).toEqual( { start: {}, end: {}, - initialPosition: null, - isMultiSelecting: false, - isEnabled: true, } ); } ); @@ -2095,8 +2023,6 @@ describe( 'state', () => { const original = deepFreeze( { start: { clientId: 'chicken' }, end: { clientId: 'chicken' }, - initialPosition: null, - isMultiSelecting: false, } ); const state = blockSelection( original, { type: 'REMOVE_BLOCKS', @@ -2110,8 +2036,6 @@ describe( 'state', () => { const original = deepFreeze( { start: { clientId: 'chicken' }, end: { clientId: 'chicken' }, - initialPosition: null, - isMultiSelecting: false, } ); const newBlock = { name: 'core/test-block', @@ -2128,9 +2052,6 @@ describe( 'state', () => { expect( state ).toEqual( { start: { clientId: 'another-block' }, end: { clientId: 'another-block' }, - initialPosition: null, - isMultiSelecting: false, - isEnabled: true, } ); } ); @@ -2138,8 +2059,6 @@ describe( 'state', () => { const original = deepFreeze( { start: { clientId: 'chicken' }, end: { clientId: 'chicken' }, - initialPosition: null, - isMultiSelecting: false, } ); const newBlock = { name: 'core/test-block', diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 4e1289e44aefc3..e5296f464b7cbf 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -1559,9 +1559,7 @@ describe( 'selectors', () => { describe( 'isSelectionEnabled', () => { it( 'should return true if selection is enable', () => { const state = { - blockSelection: { - isEnabled: true, - }, + isSelectionEnabled: true, }; expect( isSelectionEnabled( state ) ).toBe( true ); @@ -1569,9 +1567,7 @@ describe( 'selectors', () => { it( 'should return false if selection is disabled', () => { const state = { - blockSelection: { - isEnabled: false, - }, + isSelectionEnabled: false, }; expect( isSelectionEnabled( state ) ).toBe( false ); diff --git a/packages/e2e-tests/specs/undo.test.js b/packages/e2e-tests/specs/undo.test.js index 60e9ff64bfdebd..e5a58efe5063f5 100644 --- a/packages/e2e-tests/specs/undo.test.js +++ b/packages/e2e-tests/specs/undo.test.js @@ -11,6 +11,34 @@ import { saveDraft, } from '@wordpress/e2e-test-utils'; +const getSelection = async () => { + return await page.evaluate( () => { + const selectedBlock = document.activeElement.closest( '.wp-block' ); + const blocks = Array.from( document.querySelectorAll( '.wp-block' ) ); + const blockIndex = blocks.indexOf( selectedBlock ); + + if ( blockIndex === -1 ) { + return {}; + } + + const editables = Array.from( document.querySelectorAll( '[contenteditable]' ) ); + const editableIndex = editables.indexOf( document.activeElement ); + + if ( editableIndex === -1 ) { + return { blockIndex }; + } + + const { startOffset, endOffset } = window.getSelection().getRangeAt( 0 ); + + return { + blockIndex, + editableIndex, + startOffset, + endOffset, + }; + } ); +}; + describe( 'undo', () => { beforeEach( async () => { await createNewPost(); @@ -28,6 +56,13 @@ describe( 'undo', () => { await pressKeyWithModifier( 'primary', 'z' ); expect( await getEditedPostContent() ).toMatchSnapshot(); + + expect( await getSelection() ).toEqual( { + blockIndex: 1, + editableIndex: 0, + startOffset: 'before pause'.length, + endOffset: 'before pause'.length, + } ); } ); it( 'should undo typing after non input change', async () => { @@ -42,6 +77,13 @@ describe( 'undo', () => { await pressKeyWithModifier( 'primary', 'z' ); expect( await getEditedPostContent() ).toMatchSnapshot(); + + expect( await getSelection() ).toEqual( { + blockIndex: 1, + editableIndex: 0, + startOffset: 'before keyboard '.length, + endOffset: 'before keyboard '.length, + } ); } ); it( 'Should undo to expected level intervals', async () => { @@ -56,12 +98,53 @@ describe( 'undo', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); await pressKeyWithModifier( 'primary', 'z' ); // Undo 3rd paragraph text. + + expect( await getSelection() ).toEqual( { + blockIndex: 3, + editableIndex: 2, + startOffset: 0, + endOffset: 0, + } ); + await pressKeyWithModifier( 'primary', 'z' ); // Undo 3rd block. + + expect( await getSelection() ).toEqual( { + blockIndex: 2, + editableIndex: 1, + startOffset: 0, + endOffset: 0, + } ); + await pressKeyWithModifier( 'primary', 'z' ); // Undo 2nd paragraph text. + + expect( await getSelection() ).toEqual( { + blockIndex: 2, + editableIndex: 1, + startOffset: 0, + endOffset: 0, + } ); + await pressKeyWithModifier( 'primary', 'z' ); // Undo 2nd block. + + expect( await getSelection() ).toEqual( { + blockIndex: 1, + editableIndex: 0, + startOffset: 0, + endOffset: 0, + } ); + await pressKeyWithModifier( 'primary', 'z' ); // Undo 1st paragraph text. + + expect( await getSelection() ).toEqual( { + blockIndex: 1, + editableIndex: 0, + startOffset: 0, + endOffset: 0, + } ); + await pressKeyWithModifier( 'primary', 'z' ); // Undo 1st block. + expect( await getSelection() ).toEqual( {} ); expect( await getEditedPostContent() ).toBe( '' ); // After undoing every action, there should be no more undo history. expect( await page.$( '.editor-history__undo[aria-disabled="true"]' ) ).not.toBeNull(); diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index cfa67ddbb615d7..1ce8ceffa327e9 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -140,6 +140,8 @@ class EditorProvider extends Component { canUserUseUnfilteredHTML, children, blocks, + selectionStart, + selectionEnd, resetEditorBlocks, isReady, settings, @@ -164,6 +166,8 @@ class EditorProvider extends Component { value={ blocks } onInput={ resetEditorBlocksWithoutUndoLevel } onChange={ resetEditorBlocks } + selectionStart={ selectionStart } + selectionEnd={ selectionEnd } settings={ editorSettings } useSubRegistry={ false } > @@ -182,6 +186,8 @@ export default compose( [ canUserUseUnfilteredHTML, __unstableIsEditorReady: isEditorReady, getEditorBlocks, + getEditorSelectionStart, + getEditorSelectionEnd, __experimentalGetReusableBlocks, } = select( 'core/editor' ); const { canUser } = select( 'core' ); @@ -190,6 +196,8 @@ export default compose( [ canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(), isReady: isEditorReady(), blocks: getEditorBlocks(), + selectionStart: getEditorSelectionStart(), + selectionEnd: getEditorSelectionEnd(), reusableBlocks: __experimentalGetReusableBlocks(), hasUploadPermissions: defaultTo( canUser( 'create', 'media' ), true ), }; @@ -208,11 +216,18 @@ export default compose( [ setupEditor, updatePostLock, createWarningNotice, - resetEditorBlocks, + resetEditorBlocks( blocks, selectionStart, selectionEnd ) { + resetEditorBlocks( blocks, { + selectionStart, + selectionEnd, + } ); + }, updateEditorSettings, - resetEditorBlocksWithoutUndoLevel( blocks ) { + resetEditorBlocksWithoutUndoLevel( blocks, selectionStart, selectionEnd ) { resetEditorBlocks( blocks, { __unstableShouldCreateUndoLevel: false, + selectionStart, + selectionEnd, } ); }, tearDownEditor: __experimentalTearDownEditor, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 8ea2c9a408de84..55954515a74363 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -943,10 +943,18 @@ export function* resetEditorBlocks( blocks, options = {} ) { yield* resetLastBlockSourceDependencies( Array.from( updatedSources ) ); } + const { + selectionStart, + selectionEnd, + __unstableShouldCreateUndoLevel, + } = options; + return { type: 'RESET_EDITOR_BLOCKS', blocks: yield* getBlocksWithSourcedAttributes( blocks ), - shouldCreateUndoLevel: options.__unstableShouldCreateUndoLevel !== false, + selectionStart, + selectionEnd, + shouldCreateUndoLevel: __unstableShouldCreateUndoLevel !== false, }; } diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 6676b74dcbc395..0472d482fee7e1 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -136,6 +136,7 @@ export const editor = flow( [ ignoreTypes: [ 'RESET_POST', 'UPDATE_POST', + 'RESET_EDITOR_SELECTION', ], shouldOverwriteState, } ), @@ -155,6 +156,20 @@ export const editor = flow( [ return state; } ), + selectionStart( state = {}, { type, selectionStart } ) { + if ( type === 'RESET_EDITOR_BLOCKS' && selectionStart !== state ) { + return selectionStart; + } + + return state; + }, + selectionEnd( state = {}, { type, selectionEnd } ) { + if ( type === 'RESET_EDITOR_BLOCKS' && selectionEnd !== state ) { + return selectionEnd; + } + + return state; + }, edits( state = {}, action ) { switch ( action.type ) { case 'EDIT_POST': diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 7570e8b7848de6..bd04be246732e4 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1133,6 +1133,26 @@ export function getEditorBlocks( state ) { return state.editor.present.blocks.value; } +/** + * Return the current selection start object. + * + * @param {Object} state + * @return {Array} Selection start object. + */ +export function getEditorSelectionStart( state ) { + return state.editor.present.selectionStart; +} + +/** + * Return the current selection end object. + * + * @param {Object} state + * @return {Array} Selection end object. + */ +export function getEditorSelectionEnd( state ) { + return state.editor.present.selectionEnd; +} + /** * Is the editor ready * diff --git a/packages/editor/src/utils/with-history/index.js b/packages/editor/src/utils/with-history/index.js index 665851e632bd3f..de8688cf655512 100644 --- a/packages/editor/src/utils/with-history/index.js +++ b/packages/editor/src/utils/with-history/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { overSome, includes, first, last, drop, dropRight } from 'lodash'; +import { includes, first, last, drop, dropRight } from 'lodash'; /** * Default options for withHistory reducer enhancer. Refer to withHistory @@ -37,12 +37,6 @@ const DEFAULT_OPTIONS = { const withHistory = ( options = {} ) => ( reducer ) => { options = { ...DEFAULT_OPTIONS, ...options }; - // `ignoreTypes` is simply a convenience for `shouldOverwriteState` - options.shouldOverwriteState = overSome( [ - options.shouldOverwriteState, - ( action ) => includes( options.ignoreTypes, action.type ), - ] ); - const initialState = { past: [], present: reducer( undefined, {} ), @@ -120,9 +114,13 @@ const withHistory = ( options = {} ) => ( reducer ) => { let lastActionToSubmit = previousAction; if ( - shouldCreateUndoLevel || - ! past.length || - ! shouldOverwriteState( action, previousAction ) + // Ignored action should never create an undo level, regardless of + // whether there is already history or not. + ! includes( options.ignoreTypes, action.type ) && ( + shouldCreateUndoLevel || + ! past.length || + ! shouldOverwriteState( action, previousAction ) + ) ) { nextPast = [ ...past, present ]; lastActionToSubmit = action; diff --git a/packages/editor/src/utils/with-history/test/index.js b/packages/editor/src/utils/with-history/test/index.js index e383988864f77f..372b93570066c4 100644 --- a/packages/editor/src/utils/with-history/test/index.js +++ b/packages/editor/src/utils/with-history/test/index.js @@ -123,10 +123,10 @@ describe( 'withHistory', () => { state = reducer( state, { type: 'INCREMENT' } ); expect( state ).toEqual( { - past: [ 0 ], // Needs at least one history + past: [], present: 2, future: [], - lastAction: { type: 'INCREMENT' }, + lastAction: null, shouldCreateUndoLevel: false, } ); } ); diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index c2f76c4a22c296..0ce4285db558b7 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -468,8 +468,9 @@ class RichText extends Component { this.value = this.valueToFormat( record ); this.record = record; - this.props.onChange( this.value ); + // Selection must be updated first, so it is recorded in history when the content change happens. this.props.onSelectionChange( start, end ); + this.props.onChange( this.value ); this.setState( { activeFormats } ); if ( ! withoutHistory ) {