diff --git a/blocks/rich-text/index.js b/blocks/rich-text/index.js index a1f8a6709c695a..49ba70d0072d25 100644 --- a/blocks/rich-text/index.js +++ b/blocks/rich-text/index.js @@ -98,9 +98,10 @@ export default class RichText extends Component { this.onKeyUp = this.onKeyUp.bind( this ); this.changeFormats = this.changeFormats.bind( this ); this.onSelectionChange = this.onSelectionChange.bind( this ); - this.maybePropagateUndo = this.maybePropagateUndo.bind( this ); + this.onPropagateUndo = this.onPropagateUndo.bind( this ); this.onPastePreProcess = this.onPastePreProcess.bind( this ); this.onPaste = this.onPaste.bind( this ); + this.onAddUndo = this.onAddUndo.bind( this ); this.state = { formats: {}, @@ -140,15 +141,16 @@ export default class RichText extends Component { } ); editor.on( 'init', this.onInit ); - editor.on( 'focusout', this.onChange ); editor.on( 'NewBlock', this.onNewBlock ); editor.on( 'nodechange', this.onNodeChange ); editor.on( 'keydown', this.onKeyDown ); editor.on( 'keyup', this.onKeyUp ); editor.on( 'selectionChange', this.onSelectionChange ); - editor.on( 'BeforeExecCommand', this.maybePropagateUndo ); + editor.on( 'BeforeExecCommand', this.onPropagateUndo ); editor.on( 'PastePreProcess', this.onPastePreProcess, true /* Add before core handlers */ ); editor.on( 'paste', this.onPaste, true /* Add before core handlers */ ); + editor.on( 'input', this.onChange ); + editor.on( 'addundo', this.onAddUndo ); patterns.apply( this, [ editor ] ); @@ -220,23 +222,15 @@ export default class RichText extends Component { /** * Handles an undo event from tinyMCE. * - * When user attempts Undo when empty Undo stack, propagate undo - * action to context handler. The compromise here is that: TinyMCE - * handles Undo until change, at which point `editor.save` resets - * history. If no history exists, let context handler have a turn. - * Defer in case an immediate undo causes TinyMCE to be destroyed, - * if other undo behaviors test presence of an input field. - * - * @param {UndoEvent} event The undo event as triggered by tinyMCE. + * @param {UndoEvent} event The undo event as triggered by TinyMCE. */ - maybePropagateUndo( event ) { + onPropagateUndo( event ) { const { onUndo } = this.context; - if ( onUndo && event.command === 'Undo' && ! this.editor.undoManager.hasUndo() ) { - defer( onUndo ); + const { command } = event; - // We could return false here to stop other TinyMCE event handlers - // from running, but we assume TinyMCE won't do anything on an - // empty undo stack anyways. + if ( onUndo && ( command === 'Undo' || command === 'Redo' ) ) { + defer( onUndo ); + event.preventDefault(); } } @@ -358,21 +352,21 @@ export default class RichText extends Component { } } - fireChange() { - this.savedContent = this.getContent(); - this.editor.save(); - this.props.onChange( this.state.empty ? [] : this.savedContent ); - } - /** * Handles any case where the content of the tinyMCE instance has changed. */ onChange() { - // Note that due to efficiency, speed and low cost requirements isDirty may - // not reflect reality for a brief period immediately after a change. - if ( this.editor.isDirty() ) { - this.fireChange(); + this.savedContent = this.getContent(); + this.props.onChange( this.state.empty ? [] : this.savedContent ); + } + + onAddUndo( { lastLevel } ) { + if ( ! lastLevel ) { + return; } + + this.onChange(); + this.context.onCreateUndoLevel(); } /** @@ -500,7 +494,7 @@ export default class RichText extends Component { return; } - this.fireChange(); + this.context.onCreateUndoLevel(); const forward = event.keyCode === DELETE; @@ -558,6 +552,8 @@ export default class RichText extends Component { this.splitContent(); } } + + this.context.onCreateUndoLevel(); } } @@ -679,24 +675,8 @@ export default class RichText extends Component { this.setState( { formats, focusPosition, selectedNodeId: this.state.selectedNodeId + 1 } ); } - updateContent() { - const bookmark = this.editor.selection.getBookmark( 2, true ); - this.savedContent = this.props.value; - this.setContent( this.savedContent ); - this.editor.selection.moveToBookmark( bookmark ); - - // Saving the editor on updates avoid unecessary onChanges calls - // These calls can make the focus jump - this.editor.save(); - } - - setContent( content ) { - if ( ! content ) { - content = ''; - } - - content = renderToString( content ); - this.editor.setContent( content ); + setContent( content = '' ) { + this.editor.setContent( renderToString( content ) ); } getContent() { @@ -705,6 +685,7 @@ export default class RichText extends Component { componentWillUnmount() { this.onChange(); + this.context.onCreateUndoLevel(); } componentDidUpdate( prevProps ) { @@ -716,7 +697,8 @@ export default class RichText extends Component { ! isEqual( this.props.value, prevProps.value ) && ! isEqual( this.props.value, this.savedContent ) ) { - this.updateContent(); + this.savedContent = this.props.value; + this.setContent( this.savedContent ); } } @@ -849,6 +831,7 @@ export default class RichText extends Component { RichText.contextTypes = { onUndo: noop, canUserUseUnfilteredHTML: noop, + onCreateUndoLevel: noop, }; RichText.defaultProps = { diff --git a/blocks/rich-text/provider.js b/blocks/rich-text/provider.js index d8d608f5bb17ca..ede5951e8614a0 100644 --- a/blocks/rich-text/provider.js +++ b/blocks/rich-text/provider.js @@ -29,6 +29,7 @@ class RichTextProvider extends Component { RichTextProvider.childContextTypes = { onUndo: noop, + onCreateUndoLevel: noop, }; export default RichTextProvider; diff --git a/editor/components/provider/index.js b/editor/components/provider/index.js index 906092835d2c74..a0f556b3194281 100644 --- a/editor/components/provider/index.js +++ b/editor/components/provider/index.js @@ -19,7 +19,7 @@ import { /** * Internal Dependencies */ -import { setupEditor, undo, initializeMetaBoxState } from '../../store/actions'; +import { setupEditor, undo, createUndoLevel, initializeMetaBoxState } from '../../store/actions'; import store from '../../store'; /** @@ -105,10 +105,12 @@ class EditorProvider extends Component { // RichText provider: // // - context.onUndo + // - context.onCreateUndoLevel [ RichTextProvider, bindActionCreators( { onUndo: undo, + onCreateUndoLevel: createUndoLevel, }, this.store.dispatch ), ], diff --git a/editor/store/actions.js b/editor/store/actions.js index 806ed66f17ffe0..cf303bc36aee7c 100644 --- a/editor/store/actions.js +++ b/editor/store/actions.js @@ -294,6 +294,10 @@ export function undo() { return { type: 'UNDO' }; } +export function createUndoLevel() { + return { type: 'CREATE_UNDO_LEVEL' }; +} + /** * Returns an action object used in signalling that the blocks * corresponding to the specified UID set are to be removed. diff --git a/editor/store/selectors.js b/editor/store/selectors.js index 3362efb5b0a326..6b3af0a1111943 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -224,7 +224,7 @@ export function getCurrentPostLastRevisionId( state ) { * @return {Object} Object of key value pairs comprising unsaved edits. */ export function getPostEdits( state ) { - return get( state, [ 'editor', 'present', 'edits' ], {} ); + return get( state, [ 'editor', 'buffer', 'edits' ], {} ); } /** @@ -368,9 +368,10 @@ export function getDocumentTitle( state ) { * @return {string} Raw post excerpt. */ export function getEditedPostExcerpt( state ) { - return state.editor.present.edits.excerpt === undefined ? + const edits = getPostEdits( state ); + return edits.excerpt === undefined ? state.currentPost.excerpt : - state.editor.present.edits.excerpt; + edits.excerpt; } /** @@ -402,7 +403,7 @@ export function getEditedPostPreviewLink( state ) { */ export const getBlock = createSelector( ( state, uid ) => { - const block = state.editor.present.blocksByUid[ uid ]; + const block = state.editor.buffer.blocksByUid[ uid ]; if ( ! block ) { return null; } @@ -435,15 +436,15 @@ export const getBlock = createSelector( }; }, ( state ) => [ - get( state, [ 'editor', 'present', 'blocksByUid' ] ), - get( state, [ 'editor', 'present', 'edits', 'meta' ] ), + get( state, [ 'editor', 'buffer', 'blocksByUid' ] ), + get( state, [ 'editor', 'buffer', 'edits', 'meta' ] ), get( state, 'currentPost.meta' ), ] ); function getPostMeta( state, key ) { - return has( state, [ 'editor', 'present', 'edits', 'meta', key ] ) ? - get( state, [ 'editor', 'present', 'edits', 'meta', key ] ) : + return has( state, [ 'editor', 'buffer', 'edits', 'meta', key ] ) ? + get( state, [ 'editor', 'buffer', 'edits', 'meta', key ] ) : get( state, [ 'currentPost', 'meta', key ] ); } @@ -465,8 +466,8 @@ export const getBlocks = createSelector( ); }, ( state ) => [ - state.editor.present.blockOrder, - state.editor.present.blocksByUid, + state.editor.buffer.blockOrder, + state.editor.buffer.blocksByUid, ] ); @@ -525,7 +526,7 @@ export function getSelectedBlock( state ) { * @return {?string} Root UID, if exists */ export function getBlockRootUID( state, uid ) { - const { blockOrder } = state.editor.present; + const { blockOrder } = state.editor.buffer; for ( const rootUID in blockOrder ) { if ( includes( blockOrder[ rootUID ], uid ) ) { @@ -574,7 +575,7 @@ export function getAdjacentBlock( state, startUID, modifier = 1 ) { return null; } - const { blockOrder } = state.editor.present; + const { blockOrder } = state.editor.buffer; const orderSet = blockOrder[ rootUID ]; const index = orderSet.indexOf( startUID ); const nextIndex = ( index + ( 1 * modifier ) ); @@ -670,7 +671,7 @@ export const getMultiSelectedBlockUids = createSelector( return blockOrder.slice( startIndex, endIndex + 1 ); }, ( state ) => [ - state.editor.present.blockOrder, + state.editor.buffer.blockOrder, state.blockSelection.start, state.blockSelection.end, ], @@ -687,11 +688,11 @@ export const getMultiSelectedBlockUids = createSelector( export const getMultiSelectedBlocks = createSelector( ( state ) => getMultiSelectedBlockUids( state ).map( ( uid ) => getBlock( state, uid ) ), ( state ) => [ - state.editor.present.blockOrder, + state.editor.buffer.blockOrder, state.blockSelection.start, state.blockSelection.end, - state.editor.present.blocksByUid, - state.editor.present.edits.meta, + state.editor.buffer.blocksByUid, + state.editor.buffer.edits.meta, state.currentPost.meta, ] ); @@ -797,7 +798,7 @@ export function getMultiSelectedBlocksEndUid( state ) { * @return {Array} Ordered unique IDs of post blocks. */ export function getBlockOrder( state, rootUID ) { - return state.editor.present.blockOrder[ rootUID || '' ] || DEFAULT_BLOCK_ORDER; + return state.editor.buffer.blockOrder[ rootUID || '' ] || DEFAULT_BLOCK_ORDER; } /** @@ -1049,9 +1050,9 @@ export const getEditedPostContent = createSelector( return serialize( getBlocks( state ) ); }, ( state ) => [ - state.editor.present.edits.content, - state.editor.present.blocksByUid, - state.editor.present.blockOrder, + state.editor.buffer.edits.content, + state.editor.buffer.blocksByUid, + state.editor.buffer.blockOrder, ], ); diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index 1d0041de79059c..063ea9f8514be0 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -74,9 +74,9 @@ describe( 'state', () => { expect( state.past ).toEqual( [] ); expect( state.future ).toEqual( [] ); - expect( state.present.edits ).toEqual( {} ); - expect( state.present.blocksByUid ).toEqual( {} ); - expect( state.present.blockOrder ).toEqual( {} ); + expect( state.buffer.edits ).toEqual( {} ); + expect( state.buffer.blocksByUid ).toEqual( {} ); + expect( state.buffer.blockOrder ).toEqual( {} ); expect( state.isDirty ).toBe( false ); } ); @@ -87,9 +87,9 @@ describe( 'state', () => { blocks: [ { uid: 'bananas', innerBlocks: [] } ], } ); - expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 1 ); - expect( values( state.present.blocksByUid )[ 0 ].uid ).toBe( 'bananas' ); - expect( state.present.blockOrder ).toEqual( { + expect( Object.keys( state.buffer.blocksByUid ) ).toHaveLength( 1 ); + expect( values( state.buffer.blocksByUid )[ 0 ].uid ).toBe( 'bananas' ); + expect( state.buffer.blockOrder ).toEqual( { '': [ 'bananas' ], bananas: [], } ); @@ -105,8 +105,8 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 2 ); - expect( state.present.blockOrder ).toEqual( { + expect( Object.keys( state.buffer.blocksByUid ) ).toHaveLength( 2 ); + expect( state.buffer.blockOrder ).toEqual( { '': [ 'bananas' ], apples: [], bananas: [ 'apples' ], @@ -132,9 +132,9 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 2 ); - expect( values( state.present.blocksByUid )[ 1 ].uid ).toBe( 'ribs' ); - expect( state.present.blockOrder ).toEqual( { + expect( Object.keys( state.buffer.blocksByUid ) ).toHaveLength( 2 ); + expect( values( state.buffer.blocksByUid )[ 1 ].uid ).toBe( 'ribs' ); + expect( state.buffer.blockOrder ).toEqual( { '': [ 'chicken', 'ribs' ], chicken: [], ribs: [], @@ -161,10 +161,10 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 1 ); - expect( values( state.present.blocksByUid )[ 0 ].name ).toBe( 'core/freeform' ); - expect( values( state.present.blocksByUid )[ 0 ].uid ).toBe( 'wings' ); - expect( state.present.blockOrder ).toEqual( { + expect( Object.keys( state.buffer.blocksByUid ) ).toHaveLength( 1 ); + expect( values( state.buffer.blocksByUid )[ 0 ].name ).toBe( 'core/freeform' ); + expect( values( state.buffer.blocksByUid )[ 0 ].uid ).toBe( 'wings' ); + expect( state.buffer.blockOrder ).toEqual( { '': [ 'wings' ], wings: [], } ); @@ -185,7 +185,7 @@ describe( 'state', () => { blocks: [ replacementBlock ], } ); - expect( state.present.blockOrder ).toEqual( { + expect( state.buffer.blockOrder ).toEqual( { '': [ wrapperBlock.uid ], [ wrapperBlock.uid ]: [ replacementBlock.uid ], [ replacementBlock.uid ]: [], @@ -212,7 +212,7 @@ describe( 'state', () => { }, } ); - expect( state.present.blocksByUid.chicken ).toEqual( { + expect( state.buffer.blocksByUid.chicken ).toEqual( { uid: 'chicken', name: 'core/test-block', attributes: { content: 'ribs' }, @@ -240,7 +240,7 @@ describe( 'state', () => { updatedId: 3, } ); - expect( state.present.blocksByUid.chicken ).toEqual( { + expect( state.buffer.blocksByUid.chicken ).toEqual( { uid: 'chicken', name: 'core/block', attributes: { @@ -270,7 +270,7 @@ describe( 'state', () => { uids: [ 'ribs' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); + expect( state.buffer.blockOrder[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); } ); it( 'should move the nested block up', () => { @@ -287,7 +287,7 @@ describe( 'state', () => { rootUID: wrapperBlock.uid, } ); - expect( state.present.blockOrder ).toEqual( { + expect( state.buffer.blockOrder ).toEqual( { '': [ wrapperBlock.uid ], [ wrapperBlock.uid ]: [ movedBlock.uid, siblingBlock.uid ], [ movedBlock.uid ]: [], @@ -320,7 +320,7 @@ describe( 'state', () => { uids: [ 'ribs', 'veggies' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs', 'veggies', 'chicken' ] ); + expect( state.buffer.blockOrder[ '' ] ).toEqual( [ 'ribs', 'veggies', 'chicken' ] ); } ); it( 'should move multiple nested blocks up', () => { @@ -338,7 +338,7 @@ describe( 'state', () => { rootUID: wrapperBlock.uid, } ); - expect( state.present.blockOrder ).toEqual( { + expect( state.buffer.blockOrder ).toEqual( { '': [ wrapperBlock.uid ], [ wrapperBlock.uid ]: [ movedBlockA.uid, movedBlockB.uid, siblingBlock.uid ], [ movedBlockA.uid ]: [], @@ -367,7 +367,7 @@ describe( 'state', () => { uids: [ 'chicken' ], } ); - expect( state.present.blockOrder ).toBe( original.present.blockOrder ); + expect( state.buffer.blockOrder ).toBe( original.buffer.blockOrder ); } ); it( 'should move the block down', () => { @@ -390,7 +390,7 @@ describe( 'state', () => { uids: [ 'chicken' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); + expect( state.buffer.blockOrder[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); } ); it( 'should move the nested block down', () => { @@ -407,7 +407,7 @@ describe( 'state', () => { rootUID: wrapperBlock.uid, } ); - expect( state.present.blockOrder ).toEqual( { + expect( state.buffer.blockOrder ).toEqual( { '': [ wrapperBlock.uid ], [ wrapperBlock.uid ]: [ siblingBlock.uid, movedBlock.uid ], [ movedBlock.uid ]: [], @@ -440,7 +440,7 @@ describe( 'state', () => { uids: [ 'chicken', 'ribs' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'veggies', 'chicken', 'ribs' ] ); + expect( state.buffer.blockOrder[ '' ] ).toEqual( [ 'veggies', 'chicken', 'ribs' ] ); } ); it( 'should move multiple nested blocks down', () => { @@ -458,7 +458,7 @@ describe( 'state', () => { rootUID: wrapperBlock.uid, } ); - expect( state.present.blockOrder ).toEqual( { + expect( state.buffer.blockOrder ).toEqual( { '': [ wrapperBlock.uid ], [ wrapperBlock.uid ]: [ siblingBlock.uid, movedBlockA.uid, movedBlockB.uid ], [ movedBlockA.uid ]: [], @@ -487,7 +487,7 @@ describe( 'state', () => { uids: [ 'ribs' ], } ); - expect( state.present.blockOrder ).toBe( original.present.blockOrder ); + expect( state.buffer.blockOrder ).toBe( original.buffer.blockOrder ); } ); it( 'should remove the block', () => { @@ -510,9 +510,9 @@ describe( 'state', () => { uids: [ 'chicken' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); - expect( state.present.blockOrder ).not.toHaveProperty( 'chicken' ); - expect( state.present.blocksByUid ).toEqual( { + expect( state.buffer.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); + expect( state.buffer.blockOrder ).not.toHaveProperty( 'chicken' ); + expect( state.buffer.blocksByUid ).toEqual( { ribs: { uid: 'ribs', name: 'core/test-block', @@ -546,10 +546,10 @@ describe( 'state', () => { uids: [ 'chicken', 'veggies' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); - expect( state.present.blockOrder ).not.toHaveProperty( 'chicken' ); - expect( state.present.blockOrder ).not.toHaveProperty( 'veggies' ); - expect( state.present.blocksByUid ).toEqual( { + expect( state.buffer.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); + expect( state.buffer.blockOrder ).not.toHaveProperty( 'chicken' ); + expect( state.buffer.blockOrder ).not.toHaveProperty( 'veggies' ); + expect( state.buffer.blocksByUid ).toEqual( { ribs: { uid: 'ribs', name: 'core/test-block', @@ -584,8 +584,8 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 3 ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); + expect( Object.keys( state.buffer.blocksByUid ) ).toHaveLength( 3 ); + expect( state.buffer.blockOrder[ '' ] ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); } ); it( 'should remove associated blocks when deleting a reusable block', () => { @@ -609,8 +609,8 @@ describe( 'state', () => { associatedBlockUids: [ 'chicken', 'veggies' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); - expect( state.present.blocksByUid ).toEqual( { + expect( state.buffer.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); + expect( state.buffer.blocksByUid ).toEqual( { ribs: { uid: 'ribs', name: 'core/test-block', @@ -636,7 +636,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toEqual( { + expect( state.buffer.edits ).toEqual( { status: 'draft', title: 'post title', tags: [ 1 ], @@ -659,7 +659,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toBe( original.present.edits ); + expect( state.buffer.edits ).toBe( original.buffer.edits ); } ); it( 'should save modified properties', () => { @@ -680,7 +680,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toEqual( { + expect( state.buffer.edits ).toEqual( { status: 'draft', title: 'modified title', tags: [ 2 ], @@ -696,7 +696,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toEqual( { + expect( state.buffer.edits ).toEqual( { status: 'draft', title: 'post title', } ); @@ -714,7 +714,7 @@ describe( 'state', () => { }, } ); - expect( state.present.edits ).toHaveProperty( 'content' ); + expect( state.buffer.edits ).toHaveProperty( 'content' ); state = editor( original, { type: 'RESET_BLOCKS', @@ -731,7 +731,7 @@ describe( 'state', () => { } ], } ); - expect( state.present.edits ).not.toHaveProperty( 'content' ); + expect( state.buffer.edits ).not.toHaveProperty( 'content' ); } ); } ); @@ -753,7 +753,7 @@ describe( 'state', () => { }, } ); - expect( state.present.blocksByUid.kumquat.attributes.updated ).toBe( true ); + expect( state.buffer.blocksByUid.kumquat.attributes.updated ).toBe( true ); } ); it( 'should accumulate attribute block updates', () => { @@ -775,7 +775,7 @@ describe( 'state', () => { }, } ); - expect( state.present.blocksByUid.kumquat.attributes ).toEqual( { + expect( state.buffer.blocksByUid.kumquat.attributes ).toEqual( { updated: true, moreUpdated: true, } ); @@ -794,7 +794,7 @@ describe( 'state', () => { }, } ); - expect( state.present.blocksByUid ).toBe( original.present.blocksByUid ); + expect( state.buffer.blocksByUid ).toBe( original.buffer.blocksByUid ); } ); it( 'should return with same reference if no changes in updates', () => { @@ -816,7 +816,7 @@ describe( 'state', () => { }, } ); - expect( state.present.blocksByUid ).toBe( state.present.blocksByUid ); + expect( state.buffer.blocksByUid ).toBe( state.buffer.blocksByUid ); } ); } ); } ); diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index e388f792e804ba..806294d87f0425 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -250,7 +250,7 @@ describe( 'selectors', () => { status: 'auto-draft', }, editor: { - present: { + buffer: { edits: {}, }, }, @@ -265,7 +265,7 @@ describe( 'selectors', () => { status: 'draft', }, editor: { - present: { + buffer: { edits: {}, }, }, @@ -387,7 +387,7 @@ describe( 'selectors', () => { const state = { currentPost: { slug: 'old slug' }, editor: { - present: { + buffer: { edits: { slug: 'new slug', }, @@ -459,7 +459,7 @@ describe( 'selectors', () => { it( 'should return the post edits', () => { const state = { editor: { - present: { + buffer: { edits: { title: 'terga' }, }, }, @@ -476,7 +476,7 @@ describe( 'selectors', () => { title: 'sassel', }, editor: { - present: { + buffer: { edits: { status: 'private' }, }, }, @@ -491,7 +491,7 @@ describe( 'selectors', () => { title: 'sassel', }, editor: { - present: { + buffer: { edits: { title: 'youcha' }, }, }, @@ -510,7 +510,7 @@ describe( 'selectors', () => { title: 'The Title', }, editor: { - present: { + buffer: { edits: {}, blocksByUid: {}, blockOrder: {}, @@ -530,7 +530,7 @@ describe( 'selectors', () => { title: 'The Title', }, editor: { - present: { + buffer: { edits: { title: 'Modified Title', }, @@ -550,7 +550,7 @@ describe( 'selectors', () => { title: '', }, editor: { - present: { + buffer: { edits: {}, blocksByUid: {}, blockOrder: {}, @@ -571,7 +571,7 @@ describe( 'selectors', () => { title: '', }, editor: { - present: { + buffer: { edits: {}, blocksByUid: {}, blockOrder: {}, @@ -592,7 +592,7 @@ describe( 'selectors', () => { excerpt: 'sassel', }, editor: { - present: { + buffer: { edits: { status: 'private' }, }, }, @@ -607,7 +607,7 @@ describe( 'selectors', () => { excerpt: 'sassel', }, editor: { - present: { + buffer: { edits: { excerpt: 'youcha' }, }, }, @@ -624,7 +624,7 @@ describe( 'selectors', () => { status: 'draft', }, editor: { - present: { + buffer: { edits: {}, }, }, @@ -639,7 +639,7 @@ describe( 'selectors', () => { status: 'private', }, editor: { - present: { + buffer: { edits: {}, }, }, @@ -655,7 +655,7 @@ describe( 'selectors', () => { password: 'chicken', }, editor: { - present: { + buffer: { edits: {}, }, }, @@ -671,7 +671,7 @@ describe( 'selectors', () => { password: 'chicken', }, editor: { - present: { + buffer: { edits: { status: 'private', password: null, @@ -833,7 +833,7 @@ describe( 'selectors', () => { it( 'should return false if the post has no title, excerpt, content', () => { const state = { editor: { - present: { + buffer: { blocksByUid: {}, blockOrder: {}, edits: {}, @@ -848,7 +848,7 @@ describe( 'selectors', () => { it( 'should return true if the post has a title', () => { const state = { editor: { - present: { + buffer: { blocksByUid: {}, blockOrder: {}, edits: {}, @@ -865,7 +865,7 @@ describe( 'selectors', () => { it( 'should return true if the post has an excerpt', () => { const state = { editor: { - present: { + buffer: { blocksByUid: {}, blockOrder: {}, edits: {}, @@ -882,7 +882,7 @@ describe( 'selectors', () => { it( 'should return true if the post has content', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 123: { uid: 123, @@ -909,7 +909,7 @@ describe( 'selectors', () => { it( 'should return true for posts with a future date', () => { const state = { editor: { - present: { + buffer: { edits: { date: moment().add( 7, 'days' ).format( '' ) }, }, }, @@ -921,7 +921,7 @@ describe( 'selectors', () => { it( 'should return false for posts with an old date', () => { const state = { editor: { - present: { + buffer: { edits: { date: '2016-05-30T17:21:39' }, }, }, @@ -956,7 +956,7 @@ describe( 'selectors', () => { const state = { currentPost: {}, editor: { - present: { + buffer: { blocksByUid: { 123: { uid: 123, name: 'core/paragraph', attributes: {} }, }, @@ -981,7 +981,7 @@ describe( 'selectors', () => { const state = { currentPost: {}, editor: { - present: { + buffer: { blocksByUid: {}, borderOrder: {}, edits: {}, @@ -996,7 +996,7 @@ describe( 'selectors', () => { const state = { currentPost: {}, editor: { - present: { + buffer: { blocksByUid: { 123: { uid: 123, name: 'core/paragraph', attributes: {} }, 456: { uid: 456, name: 'core/paragraph', attributes: {} }, @@ -1045,7 +1045,7 @@ describe( 'selectors', () => { }, }, editor: { - present: { + buffer: { blocksByUid: { 123: { uid: 123, name: 'core/meta-block', attributes: {} }, }, @@ -1074,7 +1074,7 @@ describe( 'selectors', () => { const state = { currentPost: {}, editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1098,7 +1098,7 @@ describe( 'selectors', () => { it( 'should return the number of top-level blocks in the post', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1116,7 +1116,7 @@ describe( 'selectors', () => { it( 'should return the number of blocks in a nested context', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 123: { uid: 123, name: 'core/columns', attributes: {} }, 456: { uid: 456, name: 'core/paragraph', attributes: {} }, @@ -1139,7 +1139,7 @@ describe( 'selectors', () => { const state = { currentPost: {}, editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1156,7 +1156,7 @@ describe( 'selectors', () => { it( 'should return null if there is multi selection', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1172,7 +1172,7 @@ describe( 'selectors', () => { it( 'should return the selected block', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1200,7 +1200,7 @@ describe( 'selectors', () => { it( 'should return null if the block does not exist', () => { const state = { editor: { - present: { + buffer: { blockOrder: {}, }, }, @@ -1212,7 +1212,7 @@ describe( 'selectors', () => { it( 'should return root UID relative the block UID', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 123, 23 ], 123: [ 456, 56 ], @@ -1229,7 +1229,7 @@ describe( 'selectors', () => { it( 'should return empty if there is no multi selection', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 123, 23 ], }, @@ -1244,7 +1244,7 @@ describe( 'selectors', () => { it( 'should return selected block uids if there is multi selection', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 5, 4, 3, 2, 1 ], }, @@ -1259,7 +1259,7 @@ describe( 'selectors', () => { it( 'should return selected block uids if there is multi selection (nested context)', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 5, 4, 3, 2, 1 ], 4: [ 9, 8, 7, 6 ], @@ -1313,7 +1313,7 @@ describe( 'selectors', () => { it( 'should return the ordered block UIDs of top-level blocks by default', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 123, 23 ], }, @@ -1327,7 +1327,7 @@ describe( 'selectors', () => { it( 'should return the ordered block UIDs at a specified rootUID', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 123, 23 ], 123: [ 456 ], @@ -1344,7 +1344,7 @@ describe( 'selectors', () => { it( 'should return the block order', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 123, 23 ], }, @@ -1358,7 +1358,7 @@ describe( 'selectors', () => { it( 'should return the block order (nested context)', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 123, 23 ], 123: [ 456, 56 ], @@ -1375,7 +1375,7 @@ describe( 'selectors', () => { it( 'should return the previous block', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1398,7 +1398,7 @@ describe( 'selectors', () => { it( 'should return the previous block (nested context)', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1424,7 +1424,7 @@ describe( 'selectors', () => { it( 'should return null for the first block', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1442,7 +1442,7 @@ describe( 'selectors', () => { it( 'should return null for the first block (nested context)', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1465,7 +1465,7 @@ describe( 'selectors', () => { it( 'should return the following block', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1488,7 +1488,7 @@ describe( 'selectors', () => { it( 'should return the following block (nested context)', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1514,7 +1514,7 @@ describe( 'selectors', () => { it( 'should return null for the last block', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1532,7 +1532,7 @@ describe( 'selectors', () => { it( 'should return null for the last block (nested context)', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 23: { uid: 23, name: 'core/heading', attributes: {} }, 123: { uid: 123, name: 'core/paragraph', attributes: {} }, @@ -1582,7 +1582,7 @@ describe( 'selectors', () => { const state = { blockSelection: { start: 5, end: 3 }, editor: { - present: { + buffer: { blockOrder: { '': [ 5, 4, 3, 2, 1 ], }, @@ -1597,7 +1597,7 @@ describe( 'selectors', () => { const state = { blockSelection: { start: 5, end: 3 }, editor: { - present: { + buffer: { blockOrder: { '': [ 5, 4, 3, 2, 1 ], }, @@ -1612,7 +1612,7 @@ describe( 'selectors', () => { const state = { blockSelection: { start: 5, end: 3 }, editor: { - present: { + buffer: { blockOrder: { '': [ 5, 4, 3, 2, 1 ], }, @@ -1627,7 +1627,7 @@ describe( 'selectors', () => { const state = { blockSelection: {}, editor: { - present: { + buffer: { blockOrder: { '': [ 5, 4, 3, 2, 1 ], }, @@ -1642,7 +1642,7 @@ describe( 'selectors', () => { describe( 'isBlockMultiSelected', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 5, 4, 3, 2, 1 ], }, @@ -1663,7 +1663,7 @@ describe( 'selectors', () => { describe( 'isFirstMultiSelectedBlock', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 5, 4, 3, 2, 1 ], }, @@ -1769,7 +1769,7 @@ describe( 'selectors', () => { end: 2, }, editor: { - present: { + buffer: { blocksByUid: { 2: { uid: 2 }, }, @@ -1793,7 +1793,7 @@ describe( 'selectors', () => { end: 2, }, editor: { - present: { + buffer: { blockOrder: { '': [ 1, 2, 3 ], }, @@ -1810,7 +1810,7 @@ describe( 'selectors', () => { preferences: { mode: 'visual' }, blockSelection: { start: null, end: null }, editor: { - present: { + buffer: { blockOrder: { '': [ 1, 2, 3 ], }, @@ -1916,7 +1916,7 @@ describe( 'selectors', () => { it( 'returns null if cannot be determined', () => { const state = { editor: { - present: { + buffer: { blockOrder: {}, blocksByUid: {}, }, @@ -1929,7 +1929,7 @@ describe( 'selectors', () => { it( 'returns null if there is more than one block in the post', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 123, 456 ], }, @@ -1947,7 +1947,7 @@ describe( 'selectors', () => { it( 'returns Image if the first block is of type `core/image`', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 123 ], }, @@ -1964,7 +1964,7 @@ describe( 'selectors', () => { it( 'returns Quote if the first block is of type `core/quote`', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 456 ], }, @@ -1981,7 +1981,7 @@ describe( 'selectors', () => { it( 'returns Video if the first block is of type `core-embed/youtube`', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 567 ], }, @@ -1998,7 +1998,7 @@ describe( 'selectors', () => { it( 'returns Quote if the first block is of type `core/quote` and second is of type `core/paragraph`', () => { const state = { editor: { - present: { + buffer: { blockOrder: { '': [ 456, 789 ], }, @@ -2031,7 +2031,7 @@ describe( 'selectors', () => { it( 'should list all non-private regular block types', () => { const state = { editor: { - present: { + buffer: { blocksByUid: {}, blockOrder: {}, }, @@ -2048,7 +2048,7 @@ describe( 'selectors', () => { it( 'should properly list a regular block type', () => { const state = { editor: { - present: { + buffer: { blocksByUid: {}, blockOrder: {}, }, @@ -2075,7 +2075,7 @@ describe( 'selectors', () => { it( 'should set isDisabled when a regular block type with useOnce has been used', () => { const state = { editor: { - present: { + buffer: { blocksByUid: { 1: { uid: 1, name: 'core/test-block', attributes: {} }, }, @@ -2096,7 +2096,7 @@ describe( 'selectors', () => { it( 'should properly list reusable blocks', () => { const state = { editor: { - present: { + buffer: { blocksByUid: {}, blockOrder: {}, }, @@ -2155,7 +2155,7 @@ describe( 'selectors', () => { ], }, editor: { - present: { + buffer: { blockOrder: [], }, }, @@ -2186,7 +2186,7 @@ describe( 'selectors', () => { ], }, editor: { - present: { + buffer: { blockOrder: [], }, }, diff --git a/editor/utils/with-history/index.js b/editor/utils/with-history/index.js index f1af9e1f20573b..c34c09fcda1b1b 100644 --- a/editor/utils/with-history/index.js +++ b/editor/utils/with-history/index.js @@ -17,56 +17,84 @@ export default function withHistory( reducer, options = {} ) { const initialState = { past: [], present: reducer( undefined, {} ), + buffer: reducer( undefined, {} ), future: [], }; return ( state = initialState, action ) => { - const { past, present, future } = state; + const { past, present, buffer, future } = state; switch ( action.type ) { case 'UNDO': - // Can't undo if no past + // If there are changes in buffer, push buffer to the future. + if ( present !== buffer ) { + return { + past: past.slice( 0, past.length - 1 ), + present, + buffer: present, + future: [ buffer, ...future ], + }; + } + + // Can't undo if no past. if ( ! past.length ) { - break; + return state; } return { past: past.slice( 0, past.length - 1 ), present: past[ past.length - 1 ], + buffer: past[ past.length - 1 ], future: [ present, ...future ], }; case 'REDO': - // Can't redo if no future + // Can't redo if no future. if ( ! future.length ) { - break; + return state; } return { past: [ ...past, present ], present: future[ 0 ], + buffer: future[ 0 ], future: future.slice( 1 ), }; + + case 'CREATE_UNDO_LEVEL': + // Already has this level. + if ( present === buffer ) { + return state; + } + + return { + past: [ ...past, present ], + present: buffer, + buffer, + future: [], + }; } - const nextPresent = reducer( present, action ); + const nextBuffer = reducer( buffer, action ); if ( includes( options.resetTypes, action.type ) ) { return { past: [], - present: nextPresent, + present: nextBuffer, + buffer: nextBuffer, future: [], }; } - if ( present === nextPresent ) { + if ( buffer === nextBuffer ) { return state; } return { - past: [ ...past, present ], - present: nextPresent, - future: [], + past, + present, + buffer: nextBuffer, + future, }; }; } diff --git a/editor/utils/with-history/test/index.js b/editor/utils/with-history/test/index.js index d59dcfc39847f2..b6c507c109a8a8 100644 --- a/editor/utils/with-history/test/index.js +++ b/editor/utils/with-history/test/index.js @@ -4,8 +4,8 @@ import withHistory from '../'; describe( 'withHistory', () => { - const counter = ( state = 0, { type } ) => ( - type === 'INCREMENT' ? state + 1 : state + const counter = ( state = { count: 0 }, { type } ) => ( + type === 'INCREMENT' ? { count: state.count + 1 } : state ); it( 'should return a new reducer', () => { @@ -14,12 +14,13 @@ describe( 'withHistory', () => { expect( typeof reducer ).toBe( 'function' ); expect( reducer( undefined, {} ) ).toEqual( { past: [], - present: 0, + present: { count: 0 }, + buffer: { count: 0 }, future: [], } ); } ); - it( 'should track history', () => { + it( 'should track changes in buffer', () => { const reducer = withHistory( counter ); let state; @@ -27,28 +28,32 @@ describe( 'withHistory', () => { state = reducer( state, { type: 'INCREMENT' } ); expect( state ).toEqual( { - past: [ 0 ], - present: 1, + past: [], + present: { count: 0 }, + buffer: { count: 1 }, future: [], } ); } ); - it( 'should perform undo', () => { + it( 'should create undo level if buffer is available', () => { const reducer = withHistory( counter ); let state; state = reducer( undefined, {} ); state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'UNDO' } ); + state = reducer( state, { type: 'CREATE_UNDO_LEVEL' } ); expect( state ).toEqual( { - past: [], - present: 0, - future: [ 1 ], + past: [ { count: 0 } ], + present: { count: 1 }, + buffer: { count: 1 }, + future: [], } ); + + expect( state ).toBe( reducer( state, { type: 'CREATE_UNDO_LEVEL' } ) ); } ); - it( 'should not perform undo on empty past', () => { + it( 'should perform undo of buffer', () => { const reducer = withHistory( counter ); let state; @@ -58,40 +63,54 @@ describe( 'withHistory', () => { expect( state ).toEqual( { past: [], - present: 0, - future: [ 1 ], + present: { count: 0 }, + buffer: { count: 0 }, + future: [ { count: 1 } ], } ); + + expect( state.present ).toBe( state.buffer ); + expect( state ).toBe( reducer( state, { type: 'UNDO' } ) ); } ); - it( 'should perform redo', () => { + it( 'should perform undo of present level', () => { const reducer = withHistory( counter ); let state; state = reducer( undefined, {} ); state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'UNDO' } ); + state = reducer( state, { type: 'CREATE_UNDO_LEVEL' } ); state = reducer( state, { type: 'UNDO' } ); expect( state ).toEqual( { past: [], - present: 0, - future: [ 1 ], + present: { count: 0 }, + buffer: { count: 0 }, + future: [ { count: 1 } ], } ); + + expect( state.present ).toBe( state.buffer ); + expect( state ).toBe( reducer( state, { type: 'UNDO' } ) ); } ); - it( 'should not perform redo on empty future', () => { + it( 'should perform redo', () => { const reducer = withHistory( counter ); let state; state = reducer( undefined, {} ); state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, { type: 'CREATE_UNDO_LEVEL' } ); + state = reducer( state, { type: 'UNDO' } ); state = reducer( state, { type: 'REDO' } ); expect( state ).toEqual( { - past: [ 0 ], - present: 1, + past: [ { count: 0 } ], + present: { count: 1 }, + buffer: { count: 1 }, future: [], } ); + + expect( state.present ).toBe( state.buffer ); + expect( state ).toBe( reducer( state, { type: 'REDO' } ) ); } ); it( 'should reset history by options.resetTypes', () => { @@ -100,22 +119,14 @@ describe( 'withHistory', () => { let state; state = reducer( undefined, {} ); state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, { type: 'CREATE_UNDO_LEVEL' } ); state = reducer( state, { type: 'RESET_HISTORY' } ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'INCREMENT' } ); expect( state ).toEqual( { - past: [ 1, 2 ], - present: 3, + past: [], + present: { count: 1 }, + buffer: { count: 1 }, future: [], } ); } ); - - it( 'should return same reference if state has not changed', () => { - const reducer = withHistory( counter ); - const original = reducer( undefined, {} ); - const state = reducer( original, {} ); - - expect( state ).toBe( original ); - } ); } );