diff --git a/blocks/library/button/index.js b/blocks/library/button/index.js
index f391e6d9634512..5583b229d1ab13 100644
--- a/blocks/library/button/index.js
+++ b/blocks/library/button/index.js
@@ -60,13 +60,6 @@ registerBlockType( 'core/button', {
},
],
- getEditWrapperProps( attributes ) {
- const { align } = attributes;
- if ( 'left' === align || 'right' === align || 'center' === align ) {
- return { 'data-align': align };
- }
- },
-
edit( { attributes, setAttributes, focus, setFocus } ) {
const { text, url, title } = attributes;
diff --git a/blocks/library/embed/index.js b/blocks/library/embed/index.js
index 251a5c35b90958..07ed91217c8adb 100644
--- a/blocks/library/embed/index.js
+++ b/blocks/library/embed/index.js
@@ -65,13 +65,6 @@ registerBlockType( 'core/embed', {
},
],
- getEditWrapperProps( attributes ) {
- const { align } = attributes;
- if ( 'left' === align || 'right' === align || 'wide' === align ) {
- return { 'data-align': align };
- }
- },
-
edit: class extends wp.element.Component {
constructor() {
super( ...arguments );
diff --git a/blocks/library/embed/style.scss b/blocks/library/embed/style.scss
index 7abbf2de165903..b2e32a6778e972 100644
--- a/blocks/library/embed/style.scss
+++ b/blocks/library/embed/style.scss
@@ -35,26 +35,4 @@ div[data-type="core/embed"] {
float: right;
margin-left: $block-padding;
}
-
- &[data-align="wide"] {
- padding-left: 0;
- padding-right: 0;
- margin-right: -#{ $block-padding + $block-mover-margin }; /* Compensate for .editor-visual-editor centering padding */
-
- &:before {
- left: 0;
- border-left-width: 0;
- border-right-width: 0;
- }
-
- .editor-block-mover {
- display: none;
- }
-
- .editor-visual-editor__block-controls {
- max-width: #{ $visual-editor-max-width - $block-padding - ( $block-padding * 2 + $block-mover-margin ) };
- margin-left: auto;
- margin-right: auto;
- }
- }
}
diff --git a/blocks/library/image/index.js b/blocks/library/image/index.js
index 2b7d06ab90810a..f82ae003a9edf2 100644
--- a/blocks/library/image/index.js
+++ b/blocks/library/image/index.js
@@ -67,13 +67,6 @@ registerBlockType( 'core/image', {
},
],
- getEditWrapperProps( attributes ) {
- const { align } = attributes;
- if ( 'left' === align || 'right' === align || 'wide' === align ) {
- return { 'data-align': align };
- }
- },
-
edit( { attributes, setAttributes, focus, setFocus } ) {
const { url, alt, caption } = attributes;
diff --git a/blocks/library/image/style.scss b/blocks/library/image/style.scss
index f91f979a4d5bf2..4ba1c3a39afcb3 100644
--- a/blocks/library/image/style.scss
+++ b/blocks/library/image/style.scss
@@ -15,29 +15,6 @@
float: right;
margin-left: $block-padding;
}
-
- &[data-align="wide"] {
- padding-left: 0;
- padding-right: 0;
- margin-right: -#{ $block-padding + $block-mover-margin }; /* Compensate for .editor-visual-editor centering padding */
-
- &:before {
- left: 0;
- border-left-width: 0;
- border-right-width: 0;
- }
-
- .editor-block-mover {
- display: none;
- }
-
- .editor-visual-editor__block-controls {
- max-width: #{ $visual-editor-max-width - $block-padding - ( $block-padding * 2 + $block-mover-margin ) };
- margin-left: auto;
- margin-right: auto;
- }
-
- }
}
.blocks-image {
diff --git a/blocks/library/table/index.js b/blocks/library/table/index.js
index b14e101eeb47dd..0115dbc0e4e302 100644
--- a/blocks/library/table/index.js
+++ b/blocks/library/table/index.js
@@ -63,13 +63,6 @@ registerBlockType( 'core/table', {
},
],
- getEditWrapperProps( attributes ) {
- const { align } = attributes;
- if ( 'left' === align || 'right' === align || 'wide' === align ) {
- return { 'data-align': align };
- }
- },
-
edit( { attributes, setAttributes, focus, setFocus } ) {
const focussedKey = focus ? focus.editable || 'body.0.0' : null;
diff --git a/editor/extensions/wide-align/index.js b/editor/extensions/wide-align/index.js
new file mode 100644
index 00000000000000..78e7c9d0c56f05
--- /dev/null
+++ b/editor/extensions/wide-align/index.js
@@ -0,0 +1,54 @@
+/**
+ * WordPress dependencies
+ */
+import { registerExtension } from 'extensions';
+import { Component, cloneElement } from 'element';
+
+/**
+ * Internal dependencies
+ */
+import VisualEditorBlock from '../../modes/visual-editor/block';
+import Layout from '../../layout';
+
+registerExtension( 'wide-align', {
+ decorators: [
+ [ Layout, ( WrappedComponent ) => (
+ class extends Component {
+ componentDidMount() {
+ this.props.setExtensionState( {
+ layoutWidth: this.node.clientWidth,
+ } );
+ }
+
+ render() {
+ return (
+
this.node = node }>
+
+
+ );
+ }
+ }
+ ) ],
+ [ VisualEditorBlock, ( WrappedComponent ) => (
+ class extends Component {
+ render() {
+ const { block, extensionState } = this.props;
+ const { align } = block.attributes;
+ const element = ;
+ if ( 'wide' !== align ) {
+ return element;
+ }
+
+ const { layoutWidth } = extensionState;
+ const offset = ( ( layoutWidth / -2 ) + ( 668 / 2 ) );
+ return cloneElement( element, {
+ style: {
+ marginLeft: offset,
+ marginRight: offset,
+ },
+ } );
+ }
+ }
+ ) ],
+ ],
+} );
diff --git a/editor/index.js b/editor/index.js
index 7dc82d38a5e84d..ef2ccc29b11458 100644
--- a/editor/index.js
+++ b/editor/index.js
@@ -9,6 +9,7 @@ import { omit } from 'lodash';
* Internal dependencies
*/
import './assets/stylesheets/main.scss';
+import './extensions/wide-align';
import Layout from './layout';
import { createReduxStore } from './state';
diff --git a/editor/layout/index.js b/editor/layout/index.js
index c9e3467e8ede71..9078ff4ee2106c 100644
--- a/editor/layout/index.js
+++ b/editor/layout/index.js
@@ -3,6 +3,12 @@
*/
import { connect } from 'react-redux';
import classnames from 'classnames';
+import { flow } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { applyComponentDecorators } from 'extensions';
/**
* Internal dependencies
@@ -30,7 +36,10 @@ function Layout( { mode, isSidebarOpened } ) {
);
}
-export default connect( ( state ) => ( {
- mode: getEditorMode( state ),
- isSidebarOpened: isEditorSidebarOpened( state ),
-} ) )( Layout );
+export default flow(
+ applyComponentDecorators,
+ connect( ( state ) => ( {
+ mode: getEditorMode( state ),
+ isSidebarOpened: isEditorSidebarOpened( state ),
+ } ) )
+)( Layout );
diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js
index 1bba881de5602b..c327f0d38d1031 100644
--- a/editor/modes/visual-editor/block.js
+++ b/editor/modes/visual-editor/block.js
@@ -4,7 +4,7 @@
import { connect } from 'react-redux';
import classnames from 'classnames';
import { Slot } from 'react-slot-fill';
-import { partial } from 'lodash';
+import { partial, flow } from 'lodash';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
/**
@@ -12,6 +12,7 @@ import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
*/
import { Children } from 'element';
import { Toolbar } from 'components';
+import { applyComponentDecorators } from 'extensions';
import { BACKSPACE, ESCAPE } from 'utils/keycodes';
/**
@@ -201,12 +202,6 @@ class VisualEditorBlock extends wp.element.Component {
const { onSelect, onMouseLeave, onFocus, onInsertAfter } = this.props;
- // Determine whether the block has props to apply to the wrapper.
- let wrapperProps;
- if ( blockType.getEditWrapperProps ) {
- wrapperProps = blockType.getEditWrapperProps( block.attributes );
- }
-
// Disable reason: Each block can be selected by clicking on it
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
return (
@@ -227,7 +222,7 @@ class VisualEditorBlock extends wp.element.Component {
className={ className }
data-type={ block.name }
tabIndex="0"
- { ...wrapperProps }
+ style={ this.props.style }
>
{ ( showUI || isHovered ) && }
{ showUI &&
@@ -276,78 +271,81 @@ class VisualEditorBlock extends wp.element.Component {
}
}
-export default connect(
- ( state, ownProps ) => {
- return {
- previousBlock: getPreviousBlock( state, ownProps.uid ),
- nextBlock: getNextBlock( state, ownProps.uid ),
- block: getBlock( state, ownProps.uid ),
- isSelected: isBlockSelected( state, ownProps.uid ),
- isMultiSelected: isBlockMultiSelected( state, ownProps.uid ),
- isFirstSelected: isFirstSelectedBlock( state, ownProps.uid ),
- selectedBlocks: getSelectedBlocks( state ),
- isHovered: isBlockHovered( state, ownProps.uid ),
- focus: getBlockFocus( state, ownProps.uid ),
- isTyping: isTypingInBlock( state, ownProps.uid ),
- order: getBlockIndex( state, ownProps.uid ),
- };
- },
- ( dispatch, ownProps ) => ( {
- onChange( uid, updates ) {
- dispatch( {
- type: 'UPDATE_BLOCK',
- uid,
- updates,
- } );
- },
- onSelect() {
- dispatch( {
- type: 'TOGGLE_BLOCK_SELECTED',
- selected: true,
- uid: ownProps.uid,
- } );
- },
- onDeselect() {
- dispatch( { type: 'CLEAR_SELECTED_BLOCK' } );
- },
- onStartTyping() {
- dispatch( {
- type: 'START_TYPING',
- uid: ownProps.uid,
- } );
- },
- onHover() {
- dispatch( {
- type: 'TOGGLE_BLOCK_HOVERED',
- hovered: true,
- uid: ownProps.uid,
- } );
- },
- onMouseLeave() {
- dispatch( {
- type: 'TOGGLE_BLOCK_HOVERED',
- hovered: false,
- uid: ownProps.uid,
- } );
+export default flow(
+ applyComponentDecorators,
+ connect(
+ ( state, ownProps ) => {
+ return {
+ previousBlock: getPreviousBlock( state, ownProps.uid ),
+ nextBlock: getNextBlock( state, ownProps.uid ),
+ block: getBlock( state, ownProps.uid ),
+ isSelected: isBlockSelected( state, ownProps.uid ),
+ isMultiSelected: isBlockMultiSelected( state, ownProps.uid ),
+ isFirstSelected: isFirstSelectedBlock( state, ownProps.uid ),
+ selectedBlocks: getSelectedBlocks( state ),
+ isHovered: isBlockHovered( state, ownProps.uid ),
+ focus: getBlockFocus( state, ownProps.uid ),
+ isTyping: isTypingInBlock( state, ownProps.uid ),
+ order: getBlockIndex( state, ownProps.uid ),
+ };
},
+ ( dispatch, ownProps ) => ( {
+ onChange( uid, updates ) {
+ dispatch( {
+ type: 'UPDATE_BLOCK',
+ uid,
+ updates,
+ } );
+ },
+ onSelect() {
+ dispatch( {
+ type: 'TOGGLE_BLOCK_SELECTED',
+ selected: true,
+ uid: ownProps.uid,
+ } );
+ },
+ onDeselect() {
+ dispatch( { type: 'CLEAR_SELECTED_BLOCK' } );
+ },
+ onStartTyping() {
+ dispatch( {
+ type: 'START_TYPING',
+ uid: ownProps.uid,
+ } );
+ },
+ onHover() {
+ dispatch( {
+ type: 'TOGGLE_BLOCK_HOVERED',
+ hovered: true,
+ uid: ownProps.uid,
+ } );
+ },
+ onMouseLeave() {
+ dispatch( {
+ type: 'TOGGLE_BLOCK_HOVERED',
+ hovered: false,
+ uid: ownProps.uid,
+ } );
+ },
- onInsertAfter( block ) {
- dispatch( insertBlock( block, ownProps.uid ) );
- },
+ onInsertAfter( block ) {
+ dispatch( insertBlock( block, ownProps.uid ) );
+ },
- onFocus( ...args ) {
- dispatch( focusBlock( ...args ) );
- },
+ onFocus( ...args ) {
+ dispatch( focusBlock( ...args ) );
+ },
- onRemove( uids ) {
- dispatch( {
- type: 'REMOVE_BLOCKS',
- uids,
- } );
- },
+ onRemove( uids ) {
+ dispatch( {
+ type: 'REMOVE_BLOCKS',
+ uids,
+ } );
+ },
- onMerge( ...args ) {
- dispatch( mergeBlocks( ...args ) );
- },
- } )
+ onMerge( ...args ) {
+ dispatch( mergeBlocks( ...args ) );
+ },
+ } )
+ )
)( VisualEditorBlock );
diff --git a/editor/modes/visual-editor/style.scss b/editor/modes/visual-editor/style.scss
index ff1a0a3da33d34..5fcdc2489cbf8f 100644
--- a/editor/modes/visual-editor/style.scss
+++ b/editor/modes/visual-editor/style.scss
@@ -18,8 +18,7 @@
}
}
-/* "Hassle-free full bleed" from CSS Tricks */
-.editor-visual-editor > *:not( [data-align="wide"] ) {
+.editor-visual-editor > * {
max-width: $visual-editor-max-width;
margin-left: auto;
margin-right: auto;
diff --git a/editor/state.js b/editor/state.js
index da8f9b125570c5..2ce4e94219a261 100644
--- a/editor/state.js
+++ b/editor/state.js
@@ -5,6 +5,11 @@ import { combineReducers, applyMiddleware, createStore } from 'redux';
import refx from 'refx';
import { keyBy, first, last, omit, without, flowRight } from 'lodash';
+/**
+ * WordPress dependencies
+ */
+import { extensionsReducer } from 'extensions';
+
/**
* Internal dependencies
*/
@@ -429,6 +434,7 @@ export function createReduxStore() {
mode,
isSidebarOpened,
saving,
+ extensions: extensionsReducer,
} );
const enhancers = [ applyMiddleware( refx( effects ) ) ];
diff --git a/editor/test/state.js b/editor/test/state.js
index 5a9635c82b19cf..4e9ac11afe3cba 100644
--- a/editor/test/state.js
+++ b/editor/test/state.js
@@ -902,6 +902,7 @@ describe( 'state', () => {
'isSidebarOpened',
'saving',
'insertionPoint',
+ 'extensions',
] );
} );
} );
diff --git a/extensions/index.js b/extensions/index.js
new file mode 100644
index 00000000000000..92b1c6038eeab8
--- /dev/null
+++ b/extensions/index.js
@@ -0,0 +1,101 @@
+/**
+ * External dependencies
+ */
+import { forEach, flowRight, without } from 'lodash';
+import { connect } from 'react-redux';
+
+/**
+ * WordPress dependencies
+ */
+import { createElement, Component } from 'element';
+
+function connectExtensionState( name ) {
+ return connect(
+ ( state ) => ( {
+ extensionState: state.extensions[ name ] || {},
+ } ),
+ {
+ setExtensionState: ( state ) => {
+ return {
+ type: '@@EXTENSIONS/SET_EXTENSION_STATE',
+ name,
+ state,
+ };
+ },
+ }
+ );
+}
+
+export function applyComponentDecorators( WrappedComponent ) {
+ class DecoratedComponent extends Component {
+ static addDecorator( name, decorator ) {
+ const { instances, decorators } = this._extensions;
+
+ decorators.push( flowRight( connectExtensionState( name ), decorator ) );
+
+ this._extensions.DecoratedComponent = flowRight( ...decorators )( WrappedComponent );
+ instances.forEach( ( instance ) => instance.forceUpdate() );
+ }
+
+ componentDidMount() {
+ this.constructor._extensions.instances.push( this );
+ }
+
+ componentWillUnmount() {
+ this.constructor._extensions.instances = without(
+ this.constructor._extensions.instances,
+ this
+ );
+ }
+
+ render() {
+ return createElement(
+ this.constructor._extensions.DecoratedComponent,
+ this.props
+ );
+ }
+ }
+
+ DecoratedComponent._extensions = {
+ decorators: [],
+ instances: [],
+ DecoratedComponent: WrappedComponent,
+ };
+
+ return DecoratedComponent;
+}
+
+export function decorateComponent( DecoratedComponent, name, decorator ) {
+ if ( ! DecoratedComponent.addDecorator ) {
+ throw new TypeError( 'The provided component has not enabled decorators' );
+ }
+
+ DecoratedComponent.addDecorator( name, decorator );
+}
+
+export function registerExtension( name, settings ) {
+ forEach( settings.decorators, ( [ DecoratedComponent, decorator ] ) => {
+ decorateComponent( DecoratedComponent, name, decorator );
+ } );
+}
+
+export function extensionsReducer( state = {}, action ) {
+ switch ( action.type ) {
+ case '@@EXTENSIONS/SET_EXTENSION_STATE':
+ state = {
+ ...state,
+ [ action.name ]: {
+ ...state[ action.name ],
+ ...action.state,
+ },
+ };
+ }
+
+ return state;
+}
+
+export default {
+ applyComponentDecorators,
+ decorateComponent,
+ extensionsReducer,
+};
diff --git a/lib/client-assets.php b/lib/client-assets.php
index b8e2bc8cd36b87..c0739ad9d2bcf0 100644
--- a/lib/client-assets.php
+++ b/lib/client-assets.php
@@ -125,6 +125,12 @@ function gutenberg_register_scripts() {
array( 'react', 'react-dom', 'react-dom-server' ),
filemtime( gutenberg_dir_path() . 'element/build/index.js' )
);
+ wp_register_script(
+ 'wp-extensions',
+ gutenberg_url( 'extensions/build/index.js' ),
+ array( 'wp-element' ),
+ filemtime( gutenberg_dir_path() . 'extensions/build/index.js' )
+ );
wp_register_script(
'wp-components',
gutenberg_url( 'components/build/index.js' ),
@@ -286,7 +292,7 @@ function gutenberg_scripts_and_styles( $hook ) {
wp_enqueue_script(
'wp-editor',
gutenberg_url( 'editor/build/index.js' ),
- array( 'wp-api', 'wp-date', 'wp-i18n', 'wp-blocks', 'wp-element', 'wp-components', 'wp-utils' ),
+ array( 'wp-api', 'wp-date', 'wp-i18n', 'wp-blocks', 'wp-element', 'wp-extensions', 'wp-components', 'wp-utils' ),
filemtime( gutenberg_dir_path() . 'editor/build/index.js' ),
true // enqueue in the footer.
);
diff --git a/webpack.config.js b/webpack.config.js
index abf87b7f2b821e..6208d56c6b828e 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -8,6 +8,7 @@ const ExtractTextPlugin = require( 'extract-text-webpack-plugin' );
const entryPointNames = [
'element',
+ 'extensions',
'i18n',
'components',
'utils',