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 ) {