diff --git a/blocks/editable/index.js b/blocks/editable/index.js index ed23f2d8931cc..38a3f769e58f6 100644 --- a/blocks/editable/index.js +++ b/blocks/editable/index.js @@ -3,7 +3,17 @@ */ import tinymce from 'tinymce'; import classnames from 'classnames'; -import { last, isEqual, omitBy, forEach, merge, identity, find } from 'lodash'; +import { + last, + isEqual, + omitBy, + forEach, + merge, + identity, + find, + defer, + noop, +} from 'lodash'; import { nodeListToReact } from 'dom-react'; import { Fill } from 'react-slot-fill'; import 'element-closest'; @@ -54,6 +64,7 @@ export default class Editable 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.onPastePostProcess = this.onPastePostProcess.bind( this ); this.state = { @@ -80,6 +91,7 @@ export default class Editable extends Component { editor.on( 'keydown', this.onKeyDown ); editor.on( 'keyup', this.onKeyUp ); editor.on( 'selectionChange', this.onSelectionChange ); + editor.on( 'BeforeExecCommand', this.maybePropagateUndo ); editor.on( 'PastePostProcess', this.onPastePostProcess ); patterns.apply( this, [ editor ] ); @@ -129,6 +141,23 @@ export default class Editable extends Component { } } + maybePropagateUndo( event ) { + const { onUndo } = this.context; + if ( onUndo && event.command === 'Undo' && ! this.editor.undoManager.hasUndo() ) { + // 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. + defer( onUndo ); + + // 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. + } + } + onPastePostProcess( event ) { const childNodes = Array.from( event.node.childNodes ); const isBlockDelimiter = ( node ) => @@ -514,3 +543,7 @@ export default class Editable extends Component { ); } } + +Editable.contextTypes = { + onUndo: noop, +}; diff --git a/blocks/editable/provider.js b/blocks/editable/provider.js new file mode 100644 index 0000000000000..38aca7fdeaaeb --- /dev/null +++ b/blocks/editable/provider.js @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +import { pick, noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from 'element'; + +/** + * The Editable Provider allows a rendering context to define global behaviors + * without requiring intermediate props to be passed through to the Editable. + * The provider accepts as props its `childContextTypes` which are passed to + * any Editable instance. + */ +class EditableProvider extends Component { + getChildContext() { + return pick( + this.props, + Object.keys( this.constructor.childContextTypes ) + ); + } + + render() { + return this.props.children; + } +} + +EditableProvider.childContextTypes = { + onUndo: noop, +}; + +export default EditableProvider; diff --git a/blocks/index.js b/blocks/index.js index 9f011251f3daa..da426de0b1509 100644 --- a/blocks/index.js +++ b/blocks/index.js @@ -18,5 +18,6 @@ export { default as BlockControls } from './block-controls'; export { default as BlockDescription } from './block-description'; export { default as BlockIcon } from './block-icon'; export { default as Editable } from './editable'; +export { default as EditableProvider } from './editable/provider'; export { default as InspectorControls } from './inspector-controls'; export { default as MediaUploadButton } from './media-upload-button'; diff --git a/editor/index.js b/editor/index.js index 13961ff05cffa..8f1bac0f8d09f 100644 --- a/editor/index.js +++ b/editor/index.js @@ -1,6 +1,7 @@ /** * External dependencies */ +import { bindActionCreators } from 'redux'; import { Provider as ReduxProvider } from 'react-redux'; import { Provider as SlotFillProvider } from 'react-slot-fill'; import moment from 'moment-timezone'; @@ -9,7 +10,7 @@ import 'moment-timezone/moment-timezone-utils'; /** * WordPress dependencies */ -import { parse } from 'blocks'; +import { EditableProvider, parse } from 'blocks'; import { render } from 'element'; import { settings } from 'date'; @@ -19,6 +20,7 @@ import { settings } from 'date'; import './assets/stylesheets/main.scss'; import Layout from './layout'; import { createReduxStore } from './state'; +import { undo } from './actions'; // Configure moment globally moment.locale( settings.l10n.locale ); @@ -86,7 +88,13 @@ export function createEditorInstance( id, post ) { render( - + + + , document.getElementById( id )