Skip to content

Commit

Permalink
Perform dirty detection as diff against saved post
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Jul 24, 2017
1 parent 49b1fb3 commit 01f5a4c
Show file tree
Hide file tree
Showing 8 changed files with 545 additions and 275 deletions.
15 changes: 15 additions & 0 deletions editor/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@
import uuid from 'uuid/v4';
import { partial } from 'lodash';

/**
* Returns an action object used in signalling that blocks state should be
* 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
* @return {Object} Action object
*/
export function resetBlocks( blocks ) {
return {
type: 'RESET_BLOCKS',
blocks,
};
}

/**
* Returns an action object used in signalling that the block with the
* specified UID has been updated.
Expand Down
26 changes: 17 additions & 9 deletions editor/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@ import { get, uniqueId } from 'lodash';
/**
* WordPress dependencies
*/
import { serialize, getBlockType, switchToBlockType } from 'blocks';
import { parse, getBlockType, switchToBlockType } from 'blocks';
import { __ } from 'i18n';

/**
* Internal dependencies
*/
import { getGutenbergURL, getWPAdminURL } from './utils/url';
import { focusBlock, replaceBlocks, createSuccessNotice, createErrorNotice } from './actions';
import {
resetBlocks,
focusBlock,
replaceBlocks,
createSuccessNotice,
createErrorNotice,
} from './actions';
import {
getCurrentPost,
getCurrentPostType,
getBlocks,
getPostEdits,
getEditedPostContent,
} from './selectors';

export default {
Expand All @@ -30,19 +36,15 @@ export default {
const edits = getPostEdits( state );
const toSend = {
...edits,
content: serialize( getBlocks( state ) ),
content: getEditedPostContent( state ),
id: post.id,
};
const transactionId = uniqueId();

dispatch( {
type: 'CLEAR_POST_EDITS',
optimist: { type: BEGIN, id: transactionId },
} );
dispatch( {
type: 'UPDATE_POST',
edits: toSend,
optimist: { id: transactionId },
optimist: { type: BEGIN, id: transactionId },
} );
const Model = wp.api.getPostTypeModel( getCurrentPostType( state ) );
new Model( toSend ).save().done( ( newPost ) => {
Expand Down Expand Up @@ -207,4 +209,10 @@ export default {
]
) );
},
RESET_POST( action ) {
const { post } = action;
if ( post.content ) {
return resetBlocks( parse( post.content.raw ) );
}
},
};
9 changes: 0 additions & 9 deletions editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'moment-timezone/moment-timezone-utils';
/**
* WordPress dependencies
*/
import { parse } from 'blocks';
import { render } from 'element';
import { settings } from 'date';

Expand Down Expand Up @@ -49,14 +48,6 @@ function preparePostState( store, post ) {
post,
} );

// Parse content as blocks
if ( post.content.raw ) {
store.dispatch( {
type: 'RESET_BLOCKS',
blocks: parse( post.content.raw ),
} );
}

// Include auto draft title in edits while not flagging post as dirty
if ( post.status === 'auto-draft' ) {
store.dispatch( {
Expand Down
64 changes: 22 additions & 42 deletions editor/modes/text-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,60 +8,45 @@ import Textarea from 'react-autosize-textarea';
* WordPress dependencies
*/
import { Component } from 'element';
import { serialize, parse } from 'blocks';
import { parse } from 'blocks';

/**
* Internal dependencies
*/
import './style.scss';
import PostTitle from '../../post-title';
import { getBlocks } from '../../selectors';
import { getEditedPostContent } from '../../selectors';
import { editPost, resetBlocks } from '../../actions';

class TextEditor extends Component {
constructor( { blocks } ) {
constructor( props ) {
super( ...arguments );
const value = serialize( blocks );

this.onChange = this.onChange.bind( this );
this.onPersist = this.onPersist.bind( this );

this.state = {
blocks,
persistedValue: value,
value,
initialValue: props.value,
};
this.onChange = this.onChange.bind( this );
this.onBlur = this.onBlur.bind( this );
}

onChange( event ) {
this.setState( {
value: event.target.value,
} );
this.props.markDirty();
this.props.onChange( event.target.value );
}

onBlur() {
if ( this.state.value === this.state.persistedValue ) {
return;
}
const blocks = parse( this.state.value );
this.setState( {
blocks,
} );
this.props.onChange( blocks );
this.props.markDirty();
}
onPersist( event ) {
const { value } = event.target;
if ( value !== this.state.initialValue ) {
this.props.onPersist( value );

componentWillReceiveProps( newProps ) {
if ( newProps.blocks !== this.state.blocks ) {
const value = serialize( newProps.blocks );
this.setState( {
blocks: newProps.blocks,
persistedValue: value,
value,
initialValue: value,
} );
}
}

render() {
const { value } = this.state;
const { value } = this.props;

return (
<div className="editor-text-editor">
Expand All @@ -88,7 +73,7 @@ class TextEditor extends Component {
autoComplete="off"
value={ value }
onChange={ this.onChange }
onBlur={ this.onBlur }
onBlur={ this.onPersist }
className="editor-text-editor__textarea"
/>
</div>
Expand All @@ -99,19 +84,14 @@ class TextEditor extends Component {

export default connect(
( state ) => ( {
blocks: getBlocks( state ),
value: getEditedPostContent( state ),
} ),
{
onChange( blocks ) {
return {
type: 'RESET_BLOCKS',
blocks,
};
onChange( content ) {
return editPost( { content } );
},
markDirty() {
return {
type: 'MARK_DIRTY',
};
onPersist( content ) {
return resetBlocks( parse( content ) );
},
}
)( TextEditor );
70 changes: 61 additions & 9 deletions editor/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
* External dependencies
*/
import moment from 'moment';
import { first, last, get, values } from 'lodash';
import { first, last, values, some, isEqual } from 'lodash';
import createSelector from 'rememo';

/**
* WordPress dependencies
*/
import { getBlockType } from 'blocks';
import { serialize, getBlockType } from 'blocks';

/**
* Internal dependencies
Expand Down Expand Up @@ -89,9 +89,38 @@ export function isEditedPostNew( state ) {
* @param {Object} state Global application state
* @return {Boolean} Whether unsaved values exist
*/
export function isEditedPostDirty( state ) {
return state.editor.dirty;
}
export const isEditedPostDirty = createSelector(
( state ) => {
const edits = getPostEdits( state );
const currentPost = getCurrentPost( state );
const hasEditedAttributes = some( edits, ( value, key ) => {
return ! isEqual( value, currentPost[ key ] );
} );

if ( hasEditedAttributes ) {
return true;
}

if ( ! hasEditorUndo( state ) ) {
return false;
}

const { history } = state.editor;
return some( [
'blocksByUid',
'blockOrder',
], ( key ) => (
! isEqual(
history.past[ 0 ][ key ],
history.present[ key ]
)
) );
},
( state ) => [
state.editor,
state.currentPost,
]
);

/**
* Returns true if there are no unsaved values for the current edit session and if
Expand Down Expand Up @@ -216,7 +245,7 @@ export function isEditedPostPublishable( state ) {
*/
export function isEditedPostSaveable( state ) {
return (
getBlockCount( state ) > 0 ||
!! getEditedPostContent( state ) ||
!! getEditedPostTitle( state ) ||
!! getEditedPostExcerpt( state )
);
Expand Down Expand Up @@ -247,8 +276,8 @@ export function getEditedPostTitle( state ) {
return editedTitle;
}
const currentPost = getCurrentPost( state );
if ( currentPost.title && currentPost.title.raw ) {
return currentPost.title.raw;
if ( currentPost.title && currentPost.title ) {
return currentPost.title;
}
return '';
}
Expand Down Expand Up @@ -277,7 +306,7 @@ export function getDocumentTitle( state ) {
*/
export function getEditedPostExcerpt( state ) {
return state.editor.edits.excerpt === undefined
? get( state.currentPost, 'excerpt.raw' )
? state.currentPost.excerpt
: state.editor.edits.excerpt;
}

Expand Down Expand Up @@ -695,6 +724,29 @@ export function getSuggestedPostFormat( state ) {
return null;
}

/**
* Returns the content of the post being edited, preferring raw string edit
* before falling back to serialization of block state.
*
* @param {Object} state Global application state
* @return {String} Post content
*/
export const getEditedPostContent = createSelector(
( state ) => {
const edits = getPostEdits( state );
if ( 'content' in edits ) {
return edits.content;
}

return serialize( getBlocks( state ) );
},
( state ) => [
state.editor.edits.content,
state.editor.blocksByUid,
state.editor.blockOrder,
],
);

/**
* Returns the user notices array
*
Expand Down
Loading

0 comments on commit 01f5a4c

Please sign in to comment.