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',