diff --git a/packages/components/package.json b/packages/components/package.json
index 8e470335fa50a6..6657f37b182b61 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/components",
- "version": "7.0.2",
+ "version": "7.0.3",
"description": "UI components for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md
index 18139501460cc3..d496bb6162e239 100644
--- a/packages/data/CHANGELOG.md
+++ b/packages/data/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 4.1.0 (Unreleased)
+
+### New Feature
+
+- `withDispatch`'s `mapDispatchToProps` function takes the `registry` object as the 3rd param ([#11851](https://github.com/WordPress/gutenberg/pull/11851)).
+- `withSelect`'s `mapSelectToProps` function takes the `registry` object as the 3rd param ([#11851](https://github.com/WordPress/gutenberg/pull/11851)).
+
## 4.0.1 (2018-11-20)
## 4.0.0 (2018-11-15)
diff --git a/packages/data/README.md b/packages/data/README.md
index f6ca4c665c8fff..b5745e7719ed7e 100644
--- a/packages/data/README.md
+++ b/packages/data/README.md
@@ -229,7 +229,7 @@ A higher-order component is a function which accepts a [component](https://githu
#### `withSelect( mapSelectToProps: Function ): Function`
-Use `withSelect` to inject state-derived props into a component. Passed a function which returns an object mapping prop names to the subscribed data source, a higher-order component function is returned. The higher-order component can be used to enhance a presentational component, updating it automatically when state changes. The mapping function is passed the [`select` function](#select) and the props passed to the original component.
+Use `withSelect` to inject state-derived props into a component. Passed a function which returns an object mapping prop names to the subscribed data source, a higher-order component function is returned. The higher-order component can be used to enhance a presentational component, updating it automatically when state changes. The mapping function is passed the [`select` function](#select), the props passed to the original component and the `registry` object.
_Example:_
@@ -261,7 +261,7 @@ In the above example, when `HammerPriceDisplay` is rendered into an application,
#### `withDispatch( mapDispatchToProps: Function ): Function`
-Use `withDispatch` to inject dispatching action props into your component. Passed a function which returns an object mapping prop names to action dispatchers, a higher-order component function is returned. The higher-order component can be used to enhance a component. For example, you can define callback behaviors as props for responding to user interactions. The mapping function is passed the [`dispatch` function](#dispatch) and the props passed to the original component.
+Use `withDispatch` to inject dispatching action props into your component. Passed a function which returns an object mapping prop names to action dispatchers, a higher-order component function is returned. The higher-order component can be used to enhance a component. For example, you can define callback behaviors as props for responding to user interactions. The mapping function is passed the [`dispatch` function](#dispatch), the props passed to the original component and the `registry` object.
```jsx
function Button( { onClick, children } ) {
@@ -272,7 +272,7 @@ const { withDispatch } = wp.data;
const SaleButton = withDispatch( ( dispatch, ownProps ) => {
const { startSale } = dispatch( 'my-shop' );
- const { discountPercent = 20 } = ownProps;
+ const { discountPercent } = ownProps;
return {
onClick() {
@@ -281,6 +281,33 @@ const SaleButton = withDispatch( ( dispatch, ownProps ) => {
};
} )( Button );
+// Rendered in the application:
+//
+// Start Sale!
+```
+
+In the majority of cases, it will be sufficient to use only two first params passed to `mapDispatchToProps` as illustrated in the previous example. However, there might be some very advanced use cases where using the `registry` object might be used as a tool to optimize the performance of your component. Using `select` function from the registry might be useful when you need to fetch some dynamic data from the store at the time when the event is fired, but at the same time, you never use it to render your component. In such scenario, you can avoid using the `withSelect` higher order component to compute such prop, which might lead to unnecessary re-renders of you component caused by its frequent value change. Keep in mind, that `mapDispatchToProps` must return an object with functions only.
+
+```jsx
+function Button( { onClick, children } ) {
+ return ;
+}
+
+const { withDispatch } = wp.data;
+
+const SaleButton = withDispatch( ( dispatch, ownProps, { select } ) => {
+ // Stock number changes frequently.
+ const { getStockNumber } = select( 'my-shop' );
+ const { startSale } = dispatch( 'my-shop' );
+
+ return {
+ onClick() {
+ const dicountPercent = getStockNumber() > 50 ? 10 : 20;
+ startSale( discountPercent );
+ },
+ };
+} )( Button );
+
// Rendered in the application:
//
// Start Sale!
diff --git a/packages/data/src/components/with-dispatch/index.js b/packages/data/src/components/with-dispatch/index.js
index 3c302c557187fa..6c5944085a6942 100644
--- a/packages/data/src/components/with-dispatch/index.js
+++ b/packages/data/src/components/with-dispatch/index.js
@@ -39,7 +39,7 @@ const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent(
proxyDispatch( propName, ...args ) {
// Original dispatcher is a pre-bound (dispatching) action creator.
- mapDispatchToProps( this.props.registry.dispatch, this.props.ownProps )[ propName ]( ...args );
+ mapDispatchToProps( this.props.registry.dispatch, this.props.ownProps, this.props.registry )[ propName ]( ...args );
}
setProxyProps( props ) {
@@ -49,8 +49,12 @@ const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent(
// called, it is done only to determine the keys for which
// proxy functions should be created. The actual registry
// dispatch does not occur until the function is called.
- const propsToDispatchers = mapDispatchToProps( this.props.registry.dispatch, props.ownProps );
+ const propsToDispatchers = mapDispatchToProps( this.props.registry.dispatch, props.ownProps, this.props.registry );
this.proxyProps = mapValues( propsToDispatchers, ( dispatcher, propName ) => {
+ if ( typeof dispatcher !== 'function' ) {
+ // eslint-disable-next-line no-console
+ console.warn( `Property ${ propName } returned from mapDispatchToProps in withDispatch must be a function.` );
+ }
// Prebind with prop name so we have reference to the original
// dispatcher to invoke. Track between re-renders to avoid
// creating new function references every render.
diff --git a/packages/data/src/components/with-dispatch/test/index.js b/packages/data/src/components/with-dispatch/test/index.js
index b187c5b69410c4..c7a6fe9ac034f5 100644
--- a/packages/data/src/components/with-dispatch/test/index.js
+++ b/packages/data/src/components/with-dispatch/test/index.js
@@ -119,4 +119,67 @@ describe( 'withDispatch', () => {
expect( firstRegistryAction ).toHaveBeenCalledTimes( 2 );
expect( secondRegistryAction ).toHaveBeenCalledTimes( 2 );
} );
+
+ it( 'always calls select with the latest state in the handler passed to the component', () => {
+ const store = registry.registerStore( 'counter', {
+ reducer: ( state = 0, action ) => {
+ if ( action.type === 'update' ) {
+ return action.count;
+ }
+ return state;
+ },
+ actions: {
+ update: ( count ) => ( { type: 'update', count } ),
+ },
+ selectors: {
+ getCount: ( state ) => state,
+ },
+ } );
+
+ const Component = withDispatch( ( _dispatch, ownProps, { select: _select } ) => {
+ const outerCount = _select( 'counter' ).getCount();
+ return {
+ update: () => {
+ const innerCount = _select( 'counter' ).getCount();
+ expect( innerCount ).toBe( outerCount );
+ const actionReturnedFromDispatch = _dispatch( 'counter' ).update( innerCount + 1 );
+ expect( actionReturnedFromDispatch ).toBe( undefined );
+ },
+ };
+ } )( ( props ) => );
+
+ const testRenderer = TestRenderer.create(
+
+
+
+ );
+
+ const counterUpdateHandler = testRenderer.root.findByType( 'button' ).props.onClick;
+
+ counterUpdateHandler();
+ expect( store.getState() ).toBe( 1 );
+
+ counterUpdateHandler();
+ expect( store.getState() ).toBe( 2 );
+
+ counterUpdateHandler();
+ expect( store.getState() ).toBe( 3 );
+ } );
+
+ it( 'warns when mapDispatchToProps returns non-function property', () => {
+ const Component = withDispatch( () => {
+ return {
+ count: 3,
+ };
+ } )( () => null );
+
+ TestRenderer.create(
+
+
+
+ );
+ expect( console ).toHaveWarnedWith(
+ 'Property count returned from mapDispatchToProps in withDispatch must be a function.'
+ );
+ } );
} );
diff --git a/packages/data/src/components/with-select/index.js b/packages/data/src/components/with-select/index.js
index c471bce36388c5..d1219479d3e462 100644
--- a/packages/data/src/components/with-select/index.js
+++ b/packages/data/src/components/with-select/index.js
@@ -39,7 +39,7 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped
*/
function getNextMergeProps( props ) {
return (
- mapSelectToProps( props.registry.select, props.ownProps ) ||
+ mapSelectToProps( props.registry.select, props.ownProps, props.registry ) ||
DEFAULT_MERGE_PROPS
);
}
diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json
index c871c899256c9e..4124d52ce4cdd5 100644
--- a/packages/edit-post/package.json
+++ b/packages/edit-post/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/edit-post",
- "version": "3.1.2",
+ "version": "3.1.3",
"description": "Edit Post module for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js
index 6fb3ec96eab749..0663fcd5f98b36 100644
--- a/packages/edit-post/src/components/header/index.js
+++ b/packages/edit-post/src/components/header/index.js
@@ -82,18 +82,17 @@ function Header( {
export default compose(
withSelect( ( select ) => ( {
hasActiveMetaboxes: select( 'core/edit-post' ).hasMetaBoxes(),
- hasBlockSelection: !! select( 'core/editor' ).getBlockSelectionStart(),
isEditorSidebarOpened: select( 'core/edit-post' ).isEditorSidebarOpened(),
isPublishSidebarOpened: select( 'core/edit-post' ).isPublishSidebarOpened(),
isSaving: select( 'core/edit-post' ).isSavingMetaBoxes(),
} ) ),
- withDispatch( ( dispatch, { hasBlockSelection } ) => {
+ withDispatch( ( dispatch, ownProps, { select } ) => {
+ const { getBlockSelectionStart } = select( 'core/editor' );
const { openGeneralSidebar, closeGeneralSidebar } = dispatch( 'core/edit-post' );
- const sidebarToOpen = hasBlockSelection ? 'edit-post/block' : 'edit-post/document';
+
return {
- openGeneralSidebar: () => openGeneralSidebar( sidebarToOpen ),
+ openGeneralSidebar: () => openGeneralSidebar( getBlockSelectionStart() ? 'edit-post/block' : 'edit-post/document' ),
closeGeneralSidebar: closeGeneralSidebar,
- hasBlockSelection: undefined,
};
} ),
)( Header );
diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js
index 5a4018bc9dd1b6..1c67baed29d22c 100644
--- a/packages/edit-post/src/components/layout/index.js
+++ b/packages/edit-post/src/components/layout/index.js
@@ -37,7 +37,6 @@ import Sidebar from '../sidebar';
import PluginPostPublishPanel from '../sidebar/plugin-post-publish-panel';
import PluginPrePublishPanel from '../sidebar/plugin-pre-publish-panel';
import FullscreenMode from '../fullscreen-mode';
-import AdminNotices from '../admin-notices';
function Layout( {
mode,
@@ -71,7 +70,6 @@ function Layout( {
-
+
diff --git a/packages/edit-post/src/hooks/components/media-upload/index.js b/packages/edit-post/src/hooks/components/media-upload/index.js
index 9160f4f84c15b5..c8552168033899 100644
--- a/packages/edit-post/src/hooks/components/media-upload/index.js
+++ b/packages/edit-post/src/hooks/components/media-upload/index.js
@@ -1,7 +1,7 @@
/**
* External Dependencies
*/
-import { castArray, pick } from 'lodash';
+import { castArray, defaults, pick } from 'lodash';
/**
* WordPress dependencies
@@ -9,6 +9,8 @@ import { castArray, pick } from 'lodash';
import { Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
+const { wp } = window;
+
// Getter for the sake of unit tests.
const getGalleryDetailsMediaFrame = () => {
/**
@@ -36,7 +38,7 @@ const getGalleryDetailsMediaFrame = () => {
multiple: 'add',
editable: false,
- library: wp.media.query( _.defaults( {
+ library: wp.media.query( defaults( {
type: 'image',
}, this.options.library ) ),
} ),
diff --git a/packages/edit-post/src/prevent-event-discovery.js b/packages/edit-post/src/prevent-event-discovery.js
new file mode 100644
index 00000000000000..97e7b7d8a8fbbf
--- /dev/null
+++ b/packages/edit-post/src/prevent-event-discovery.js
@@ -0,0 +1,17 @@
+export default {
+ 't a l e s o f g u t e n b e r g': ( event ) => {
+ if (
+ ! document.activeElement.classList.contains( 'edit-post-visual-editor' ) &&
+ document.activeElement !== document.body
+ ) {
+ return;
+ }
+
+ event.preventDefault();
+ window.wp.data.dispatch( 'core/editor' ).insertBlock(
+ window.wp.blocks.createBlock( 'core/paragraph', {
+ content: '๐ก๐ข๐ฆ๐ค๐ฆ๐๐ง๐น๐ฆ๐ฆ๐ฆ๐ผ๐ฟ๐๐ด๐๐๐ฆ๐ฆ๐ฑ๐ฯ๐๐๐ง๐ฅจ๐๐๐ ๐ฅฆ๐ฅ๐ฅ๐๐ฅฅ๐ฅ๐ต๐ฅ๐๐ฏ๐พ๐ฒ๐บ๐๐ฎโ๏ธ',
+ } )
+ );
+ },
+};
diff --git a/packages/editor/package.json b/packages/editor/package.json
index 45f6c934df1b1f..831c8ef25adf61 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/editor",
- "version": "9.0.2",
+ "version": "9.0.3",
"description": "Building blocks for WordPress editors.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/editor/src/components/autocompleters/block.js b/packages/editor/src/components/autocompleters/block.js
index e235c9a384aadb..d40b18d55c38bc 100644
--- a/packages/editor/src/components/autocompleters/block.js
+++ b/packages/editor/src/components/autocompleters/block.js
@@ -68,8 +68,8 @@ export function createBlockCompleter( {
);
},
getOptionKeywords( inserterItem ) {
- const { title, keywords = [] } = inserterItem;
- return [ ...keywords, title ];
+ const { title, keywords = [], category } = inserterItem;
+ return [ category, ...keywords, title ];
},
getOptionLabel( inserterItem ) {
const { icon, title } = inserterItem;
diff --git a/packages/editor/src/components/autocompleters/test/block.js b/packages/editor/src/components/autocompleters/test/block.js
index 56299cba2514aa..8b853cd6931732 100644
--- a/packages/editor/src/components/autocompleters/test/block.js
+++ b/packages/editor/src/components/autocompleters/test/block.js
@@ -40,30 +40,33 @@ describe( 'block', () => {
expect( completer.options() ).toEqual( [ option1, option3 ] );
} );
- it( 'should derive option keywords from block keywords and block title', () => {
+ it( 'should derive option keywords from block category, block keywords and block title', () => {
const inserterItemWithTitleAndKeywords = {
name: 'core/foo',
title: 'foo',
keywords: [ 'foo-keyword-1', 'foo-keyword-2' ],
+ category: 'formatting',
};
const inserterItemWithTitleAndEmptyKeywords = {
name: 'core/bar',
title: 'bar',
// Intentionally empty keyword list
keywords: [],
+ category: 'common',
};
const inserterItemWithTitleAndUndefinedKeywords = {
name: 'core/baz',
title: 'baz',
+ category: 'widgets',
// Intentionally omitted keyword list
};
expect( blockCompleter.getOptionKeywords( inserterItemWithTitleAndKeywords ) )
- .toEqual( [ 'foo-keyword-1', 'foo-keyword-2', 'foo' ] );
+ .toEqual( [ 'formatting', 'foo-keyword-1', 'foo-keyword-2', 'foo' ] );
expect( blockCompleter.getOptionKeywords( inserterItemWithTitleAndEmptyKeywords ) )
- .toEqual( [ 'bar' ] );
+ .toEqual( [ 'common', 'bar' ] );
expect( blockCompleter.getOptionKeywords( inserterItemWithTitleAndUndefinedKeywords ) )
- .toEqual( [ 'baz' ] );
+ .toEqual( [ 'widgets', 'baz' ] );
} );
it( 'should render a block option label', () => {
diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js
index e57a221531dcc0..98956980d0d555 100644
--- a/packages/editor/src/components/block-list/block.js
+++ b/packages/editor/src/components/block-list/block.js
@@ -71,7 +71,6 @@ export class BlockListBlock extends Component {
this.onDragStart = this.onDragStart.bind( this );
this.onDragEnd = this.onDragEnd.bind( this );
this.selectOnOpen = this.selectOnOpen.bind( this );
- this.onShiftSelection = this.onShiftSelection.bind( this );
this.hadTouchStart = false;
this.state = {
@@ -290,7 +289,7 @@ export class BlockListBlock extends Component {
if ( event.shiftKey ) {
if ( ! this.props.isSelected ) {
- this.onShiftSelection();
+ this.props.onShiftSelection();
event.preventDefault();
}
} else {
@@ -362,20 +361,6 @@ export class BlockListBlock extends Component {
}
}
- onShiftSelection() {
- if ( ! this.props.isSelectionEnabled ) {
- return;
- }
-
- const { getBlockSelectionStart, onMultiSelect, onSelect } = this.props;
-
- if ( getBlockSelectionStart() ) {
- onMultiSelect( getBlockSelectionStart(), this.props.clientId );
- } else {
- onSelect( this.props.clientId );
- }
- }
-
forceFocusedContextualToolbar() {
this.isForcingContextualToolbar = true;
// trigger a re-render
@@ -649,7 +634,6 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV
getEditorSettings,
hasSelectedInnerBlock,
getTemplateLock,
- getBlockSelectionStart,
} = select( 'core/editor' );
const isSelected = isBlockSelected( clientId );
const { hasFixedToolbar, focusMode } = getEditorSettings();
@@ -682,13 +666,11 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV
block,
isSelected,
isParentOfSelectedBlock,
- // We only care about this value when the shift key is pressed.
- // We call it dynamically in the event handler to avoid unnecessary re-renders.
- getBlockSelectionStart,
};
} );
-const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => {
+const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => {
+ const { getBlockSelectionStart } = select( 'core/editor' );
const {
updateBlockAttributes,
selectBlock,
@@ -709,7 +691,6 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => {
onSelect( clientId = ownProps.clientId, initialPosition ) {
selectBlock( clientId, initialPosition );
},
- onMultiSelect: multiSelect,
onInsertBlocks( blocks, index ) {
const { rootClientId } = ownProps;
insertBlocks( blocks, index, rootClientId );
@@ -730,6 +711,17 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => {
onMetaChange( meta ) {
editPost( { meta } );
},
+ onShiftSelection() {
+ if ( ! ownProps.isSelectionEnabled ) {
+ return;
+ }
+
+ if ( getBlockSelectionStart() ) {
+ multiSelect( getBlockSelectionStart(), ownProps.clientId );
+ } else {
+ selectBlock( ownProps.clientId );
+ }
+ },
toggleSelection( selectionEnabled ) {
toggleSelection( selectionEnabled );
},
diff --git a/packages/editor/src/components/copy-handler/index.js b/packages/editor/src/components/copy-handler/index.js
index e74cde91bd2aa2..f9da173bf03058 100644
--- a/packages/editor/src/components/copy-handler/index.js
+++ b/packages/editor/src/components/copy-handler/index.js
@@ -4,7 +4,7 @@
import { Component } from '@wordpress/element';
import { serialize } from '@wordpress/blocks';
import { documentHasSelection } from '@wordpress/dom';
-import { withSelect, withDispatch } from '@wordpress/data';
+import { withDispatch } from '@wordpress/data';
import { compose } from '@wordpress/compose';
class CopyHandler extends Component {
@@ -26,33 +26,13 @@ class CopyHandler extends Component {
}
onCopy( event ) {
- const { hasMultiSelection, selectedBlockClientIds, getBlocksByClientId } = this.props;
-
- if ( selectedBlockClientIds.length === 0 ) {
- return;
- }
-
- // Let native copy behaviour take over in input fields.
- if ( ! hasMultiSelection && documentHasSelection() ) {
- return;
- }
-
- const serialized = serialize( getBlocksByClientId( selectedBlockClientIds ) );
-
- event.clipboardData.setData( 'text/plain', serialized );
- event.clipboardData.setData( 'text/html', serialized );
-
+ this.props.onCopy( event.clipboardData );
event.preventDefault();
}
onCut( event ) {
- const { hasMultiSelection, selectedBlockClientIds } = this.props;
-
- this.onCopy( event );
-
- if ( hasMultiSelection ) {
- this.props.onRemove( selectedBlockClientIds );
- }
+ this.props.onCut( event.clipboardData );
+ event.preventDefault();
}
render() {
@@ -61,27 +41,41 @@ class CopyHandler extends Component {
}
export default compose( [
- withSelect( ( select ) => {
+ withDispatch( ( dispatch, ownProps, { select } ) => {
const {
+ getBlocksByClientId,
getMultiSelectedBlockClientIds,
getSelectedBlockClientId,
- getBlocksByClientId,
hasMultiSelection,
} = select( 'core/editor' );
+ const { removeBlocks } = dispatch( 'core/editor' );
const selectedBlockClientId = getSelectedBlockClientId();
const selectedBlockClientIds = selectedBlockClientId ? [ selectedBlockClientId ] : getMultiSelectedBlockClientIds();
return {
- hasMultiSelection: hasMultiSelection(),
- selectedBlockClientIds,
-
- // We only care about this value when the copy is performed
- // We call it dynamically in the event handler to avoid unnecessary re-renders.
- getBlocksByClientId,
+ onCopy( dataTransfer ) {
+ if ( selectedBlockClientIds.length === 0 ) {
+ return;
+ }
+
+ // Let native copy behaviour take over in input fields.
+ if ( ! hasMultiSelection() && documentHasSelection() ) {
+ return;
+ }
+
+ const serialized = serialize( getBlocksByClientId( selectedBlockClientIds ) );
+
+ dataTransfer.setData( 'text/plain', serialized );
+ dataTransfer.setData( 'text/html', serialized );
+ },
+ onCut( dataTransfer ) {
+ this.onCopy( dataTransfer );
+
+ if ( hasMultiSelection() ) {
+ removeBlocks( selectedBlockClientIds );
+ }
+ },
};
} ),
- withDispatch( ( dispatch ) => ( {
- onRemove: dispatch( 'core/editor' ).removeBlocks,
- } ) ),
] )( CopyHandler );
diff --git a/packages/editor/src/components/media-placeholder/index.native.js b/packages/editor/src/components/media-placeholder/index.native.js
index 008bfba7df213e..6a64523a43962c 100644
--- a/packages/editor/src/components/media-placeholder/index.native.js
+++ b/packages/editor/src/components/media-placeholder/index.native.js
@@ -12,10 +12,9 @@ function MediaPlaceholder( props ) {
Image
- Upload a new image or select a file from your library.
+ Select an image from your library.
-
diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js
index 688a5287052b68..57cea883c75c90 100644
--- a/packages/editor/src/components/rich-text/index.js
+++ b/packages/editor/src/components/rich-text/index.js
@@ -109,6 +109,7 @@ export class RichText extends Component {
this.valueToFormat = this.valueToFormat.bind( this );
this.setRef = this.setRef.bind( this );
this.isActive = this.isActive.bind( this );
+ this.valueToEditableHTML = this.valueToEditableHTML.bind( this );
this.formatToValue = memize( this.formatToValue.bind( this ), { size: 1 } );
@@ -874,7 +875,8 @@ export class RichText extends Component {
tagName={ Tagname }
onSetup={ this.onSetup }
style={ style }
- defaultValue={ this.valueToEditableHTML( record ) }
+ record={ record }
+ valueToEditableHTML={ this.valueToEditableHTML }
isPlaceholderVisible={ isPlaceholderVisible }
aria-label={ placeholder }
aria-autocomplete="list"
diff --git a/packages/editor/src/components/rich-text/index.native.js b/packages/editor/src/components/rich-text/index.native.js
index ba6a4673e101c2..362dc4b359d1a3 100644
--- a/packages/editor/src/components/rich-text/index.native.js
+++ b/packages/editor/src/components/rich-text/index.native.js
@@ -268,17 +268,12 @@ export class RichText extends Component {
this.lastContent = undefined;
return true;
}
- // The check below allows us to avoid updating the content right after an `onChange` call.
- // The first time the component is drawn `lastContent` and `lastEventCount ` are both undefined
- if ( this.lastEventCount &&
- nextProps.value &&
- this.lastContent &&
- nextProps.value === this.lastContent ) {
- return false;
- }
- // If the component is changed React side (merging/splitting/custom text actions) we need to make sure
- // the native is updated as well
+ // TODO: Please re-introduce the check to avoid updating the content right after an `onChange` call.
+ // It was removed in https://github.com/WordPress/gutenberg/pull/12417 to fix undo/redo problem.
+
+ // If the component is changed React side (undo/redo/merging/splitting/custom text actions)
+ // we need to make sure the native is updated as well
if ( nextProps.value &&
this.lastContent &&
nextProps.value !== this.lastContent ) {
@@ -288,6 +283,18 @@ export class RichText extends Component {
return true;
}
+ componentDidMount() {
+ if ( this.props.isSelected ) {
+ this._editor.focus();
+ }
+ }
+
+ componentDidUpdate( prevProps ) {
+ if ( this.props.isSelected && ! prevProps.isSelected ) {
+ this._editor.focus();
+ }
+ }
+
isFormatActive( format ) {
return this.state.formats[ format ] && this.state.formats[ format ].isActive;
}
diff --git a/packages/editor/src/components/rich-text/style.scss b/packages/editor/src/components/rich-text/style.scss
index 731809331440cb..0907de79535d12 100644
--- a/packages/editor/src/components/rich-text/style.scss
+++ b/packages/editor/src/components/rich-text/style.scss
@@ -8,6 +8,16 @@
margin: 0;
position: relative;
line-height: $editor-line-height;
+ // In HTML, leading and trailing spaces are not visible, and multiple spaces
+ // elsewhere are visually reduced to one space. This rule prevents spaces
+ // from collapsing so all space is visible in the editor and can be removed.
+ // It also prevents some browsers from inserting non-breaking spaces at the
+ // end of a line to prevent the space from visually disappearing. Sometimes
+ // these non breaking spaces can linger in the editor causing unwanted non
+ // breaking spaces in between words. If also prevent Firefox from inserting
+ // a trailing `br` node to visualise any trailing space, causing the element
+ // to be saved.
+ white-space: pre-wrap;
> p:empty {
min-height: $editor-font-size * $editor-line-height;
diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js
index 833a740c690ece..3a825fb995afad 100644
--- a/packages/editor/src/components/rich-text/tinymce.js
+++ b/packages/editor/src/components/rich-text/tinymce.js
@@ -111,6 +111,7 @@ export default class TinyMCE extends Component {
this.bindEditorNode = this.bindEditorNode.bind( this );
this.onFocus = this.onFocus.bind( this );
this.onKeyDown = this.onKeyDown.bind( this );
+ this.initialize = this.initialize.bind( this );
}
onFocus() {
@@ -167,7 +168,16 @@ export default class TinyMCE extends Component {
}
}
+ /**
+ * Initializes TinyMCE. Can only be called once per instance.
+ */
initialize() {
+ if ( this.initialize.called ) {
+ return;
+ }
+
+ this.initialize.called = true;
+
const { multilineTag } = this.props;
const settings = {
theme: false,
@@ -332,7 +342,8 @@ export default class TinyMCE extends Component {
const {
tagName = 'div',
style,
- defaultValue,
+ record,
+ valueToEditableHTML,
className,
isPlaceholderVisible,
onPaste,
@@ -362,7 +373,7 @@ export default class TinyMCE extends Component {
ref: this.bindEditorNode,
style,
suppressContentEditableWarning: true,
- dangerouslySetInnerHTML: { __html: defaultValue },
+ dangerouslySetInnerHTML: { __html: valueToEditableHTML( record ) },
onPaste,
onInput,
onFocus: this.onFocus,
diff --git a/packages/eslint-config/README.md b/packages/eslint-config/README.md
new file mode 100644
index 00000000000000..5f2b003e765c5d
--- /dev/null
+++ b/packages/eslint-config/README.md
@@ -0,0 +1,24 @@
+# ESLint Config
+
+[ESLint](https://eslint.org/) config for WordPress development.
+
+## Installation
+
+Install the module
+
+```bash
+npm install @wordpress/eslint-config --save-dev
+```
+
+### Usage
+
+Next, extend the configuration from your project's `.eslintrc` file:
+
+```json
+"extends": "@wordpress/eslint-config"
+```
+
+Refer to the [ESLint documentation on Shareable Configs](http://eslint.org/docs/developer-guide/shareable-configs) for more information.
+
+
+
diff --git a/packages/eslint-config/configs/es5.js b/packages/eslint-config/configs/es5.js
new file mode 100644
index 00000000000000..2e56e7a2f5fcdb
--- /dev/null
+++ b/packages/eslint-config/configs/es5.js
@@ -0,0 +1,12 @@
+/**
+ * The original version of this file is based on WordPress ESLint rules and shared configs:
+ * https://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.
+ */
+
+module.exports = {
+ env: {
+ es6: true,
+ },
+
+ rules: require( './rules/esnext' ),
+};
diff --git a/packages/eslint-config/configs/esnext.js b/packages/eslint-config/configs/esnext.js
new file mode 100644
index 00000000000000..3b3a80d7d4cd82
--- /dev/null
+++ b/packages/eslint-config/configs/esnext.js
@@ -0,0 +1,8 @@
+/**
+ * The original version of this file is based on WordPress ESLint rules and shared configs:
+ * https://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.
+ */
+
+module.exports = {
+ rules: require( './rules/es5' ),
+};
diff --git a/packages/eslint-config/configs/rules/es5.js b/packages/eslint-config/configs/rules/es5.js
new file mode 100644
index 00000000000000..61e3e01c343f1d
--- /dev/null
+++ b/packages/eslint-config/configs/rules/es5.js
@@ -0,0 +1,84 @@
+module.exports = {
+ // Possible Errors
+ // Disallow assignment in conditional expressions
+ 'no-cond-assign': [ 'error', 'except-parens' ],
+ // Disallow irregular whitespace outside of strings and comments
+ 'no-irregular-whitespace': 'error',
+ // Best Practices
+ // Specify curly brace conventions for all control statements
+ curly: [ 'error', 'all' ],
+ // Encourages use of dot notation whenever possible
+ 'dot-notation': [ 'error', {
+ allowKeywords: true,
+ allowPattern: '^[a-z]+(_[a-z]+)+$',
+ } ],
+ // Disallow use of multiline strings
+ 'no-multi-str': 'error',
+ // Disallow use of the with statement
+ 'no-with': 'error',
+ // Requires to declare all vars on top of their containing scope
+ 'vars-on-top': 'error',
+ // Require immediate function invocation to be wrapped in parentheses
+ 'wrap-iife': 'error',
+ // Require or disallow Yoda conditions
+ yoda: [ 'error', 'always' ],
+ // Strict Mode
+ // Variables
+ // Stylistic Issues
+ // Enforce spacing inside array brackets
+ 'array-bracket-spacing': [ 'error', 'always' ],
+ // Enforce one true brace style
+ 'brace-style': 'error',
+ // Require camel case names
+ camelcase: [ 'error', {
+ properties: 'always',
+ } ],
+ // Disallow or enforce trailing commas
+ 'comma-dangle': [ 'error', 'never' ],
+ // Enforce spacing before and after comma
+ 'comma-spacing': 'error',
+ // Enforce one true comma style
+ 'comma-style': [ 'error', 'last' ],
+ // Enforce newline at the end of file, with no multiple empty lines
+ 'eol-last': 'error',
+ // Enforces spacing between keys and values in object literal properties
+ 'key-spacing': [ 'error', {
+ beforeColon: false,
+ afterColon: true,
+ } ],
+ // Enforce spacing before and after keywords
+ 'keyword-spacing': 'error',
+ // Disallow mixed "LF" and "CRLF" as linebreaks
+ 'linebreak-style': [ 'error', 'unix' ],
+ // Enforces empty lines around comments
+ 'lines-around-comment': [ 'error', {
+ beforeLineComment: true,
+ } ],
+ // Disallow mixed spaces and tabs for indentation
+ 'no-mixed-spaces-and-tabs': 'error',
+ // Disallow multiple empty lines
+ 'no-multiple-empty-lines': 'error',
+ // Disallow trailing whitespace at the end of lines
+ 'no-trailing-spaces': 'error',
+ // Require or disallow an newline around variable declarations
+ 'one-var-declaration-per-line': [ 'error', 'initializations' ],
+ // Enforce operators to be placed before or after line breaks
+ 'operator-linebreak': [ 'error', 'after' ],
+ // Specify whether backticks, double or single quotes should be used
+ quotes: [ 'error', 'single' ],
+ // Require or disallow use of semicolons instead of ASI
+ semi: [ 'error', 'always' ],
+ // Require or disallow space before blocks
+ 'space-before-blocks': [ 'error', 'always' ],
+ // Require or disallow space before function opening parenthesis
+ 'space-before-function-paren': [ 'error', 'never' ],
+ // Require or disallow space before blocks
+ 'space-in-parens': [ 'error', 'always', { exceptions: [ '{}', '[]' ] } ],
+ // Require spaces around operators
+ 'space-infix-ops': 'error',
+ // Require or disallow spaces before/after unary operators (words on by default, nonwords)
+ 'space-unary-ops': [ 'error', {
+ overrides: { '!': true },
+ } ],
+ // Legacy
+};
diff --git a/packages/eslint-config/configs/rules/esnext.js b/packages/eslint-config/configs/rules/esnext.js
new file mode 100644
index 00000000000000..dcfc27f06554f3
--- /dev/null
+++ b/packages/eslint-config/configs/rules/esnext.js
@@ -0,0 +1,66 @@
+// see https://eslint.org/docs/rules/#ecmascript-6
+//
+module.exports = {
+ // require braces around arrow function bodies
+ 'arrow-body-style': 'off',
+ // require parentheses around arrow function arguments
+ 'arrow-parens': 'off',
+ // enforce consistent spacing before and after the arrow in arrow functions
+ 'arrow-spacing': 'off',
+ // require super() calls in constructors
+ 'constructor-super': 'error',
+ // enforce consistent spacing around * operators in generator functions
+ 'generator-star-spacing': 'off',
+ // disallow reassigning class members
+ 'no-class-assign': 'off',
+ // disallow arrow functions where they could be confused with comparisons
+ 'no-confusing-arrow': 'off',
+ // disallow reassigning `const` variables
+ 'no-const-assign': 'error',
+ // disallow duplicate class members
+ 'no-dupe-class-members': 'error',
+ // disallow duplicate module imports
+ 'no-duplicate-imports': 'error',
+ // disallow `new` operators with the `Symbol` object
+ 'no-new-symbol': 'off',
+ // disallow specified modules when loaded by `import`
+ 'no-restricted-imports': 'off',
+ // disallow `this`/`super` before calling `super()` in constructors
+ 'no-this-before-super': 'off',
+ // disallow unnecessary computed property keys in object literals
+ 'no-useless-computed-key': 'error',
+ // disallow unnecessary constructors
+ 'no-useless-constructor': 'error',
+ // disallow renaming import, export, and destructured assignments to the same name
+ 'no-useless-rename': 'off',
+ // require `let` or `const` instead of `var`
+ 'no-var': 'error',
+ // require or disallow method and property shorthand syntax for object literals
+ 'object-shorthand': 'off',
+ // require arrow functions as callbacks
+ 'prefer-arrow-callback': 'off',
+ // require `const` declarations for variables that are never reassigned after declared
+ 'prefer-const': 'error',
+ // require destructuring from arrays and/or objects
+ 'prefer-destructuring': 'off',
+ // disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal literals
+ 'prefer-numeric-literals': 'off',
+ // require rest parameters instead of `arguments`
+ 'prefer-rest-params': 'off',
+ // require spread operators instead of `.apply()`
+ 'prefer-spread': 'off',
+ // require template literals instead of string concatenation
+ 'prefer-template': 'off',
+ // require generator functions to contain `yield`
+ 'require-yield': 'off',
+ // enforce spacing between rest and spread operators and their expressions
+ 'rest-spread-spacing': 'off',
+ // enforce sorted import declarations within modules
+ 'sort-imports': 'off',
+ // require symbol descriptions
+ 'symbol-description': 'off',
+ // require or disallow spacing around embedded expressions of template strings
+ 'template-curly-spacing': [ 'error', 'always' ],
+ // require or disallow spacing around the `*` in `yield*` expressions
+ 'yield-star-spacing': 'off',
+};
diff --git a/eslint/config.js b/packages/eslint-config/index.js
similarity index 97%
rename from eslint/config.js
rename to packages/eslint-config/index.js
index 5ba1ba4f43e843..aa02fdc33ff9d5 100644
--- a/eslint/config.js
+++ b/packages/eslint-config/index.js
@@ -1,14 +1,12 @@
module.exports = {
parser: 'babel-eslint',
extends: [
- 'wordpress',
- 'plugin:wordpress/esnext',
+ './configs/es5.js',
+ './configs/esnext.js',
'plugin:react/recommended',
'plugin:jsx-a11y/recommended',
],
env: {
- browser: false,
- es6: true,
node: true,
},
parserOptions: {
@@ -22,7 +20,6 @@ module.exports = {
document: true,
},
plugins: [
- 'wordpress',
'react',
'jsx-a11y',
],
diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json
new file mode 100644
index 00000000000000..07a609d7278fa2
--- /dev/null
+++ b/packages/eslint-config/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@wordpress/eslint-config",
+ "version": "1.0.0-alpha.0",
+ "description": "ESLint config for WordPress development.",
+ "author": "The WordPress Contributors",
+ "license": "GPL-2.0-or-later",
+ "keywords": [
+ "wordpress",
+ "eslint"
+ ],
+ "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/eslint-config/README.md",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/WordPress/gutenberg.git"
+ },
+ "bugs": {
+ "url": "https://github.com/WordPress/gutenberg/issues"
+ },
+ "dependencies": {
+ "babel-eslint": "^8.0.3",
+ "eslint-plugin-jsx-a11y": "6.0.2",
+ "eslint-plugin-react": "7.7.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/format-library/package.json b/packages/format-library/package.json
index 9fa59207801c16..01f9cdd51de698 100644
--- a/packages/format-library/package.json
+++ b/packages/format-library/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/format-library",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Format library for the WordPress editor.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json
index 190171efe08511..8c455e8a458125 100644
--- a/packages/list-reusable-blocks/package.json
+++ b/packages/list-reusable-blocks/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/list-reusable-blocks",
- "version": "1.1.15",
+ "version": "1.1.16",
"description": "Adding Export/Import support to the reusable blocks listing.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/notices/package.json b/packages/notices/package.json
index 03ca94a9c9bb6c..f7ae5027587cdb 100644
--- a/packages/notices/package.json
+++ b/packages/notices/package.json
@@ -16,10 +16,6 @@
"bugs": {
"url": "https://github.com/WordPress/gutenberg/issues"
},
- "files": [
- "build",
- "build-module"
- ],
"main": "build/index.js",
"react-native": "src/index",
"dependencies": {
diff --git a/packages/nux/package.json b/packages/nux/package.json
index 82cbc79c4650d5..d754dc9f891f5a 100644
--- a/packages/nux/package.json
+++ b/packages/nux/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/nux",
- "version": "3.0.3",
+ "version": "3.0.4",
"description": "NUX (New User eXperience) module for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/nux/src/components/dot-tip/index.js b/packages/nux/src/components/dot-tip/index.js
index 022eb745352419..cd2356f8026690 100644
--- a/packages/nux/src/components/dot-tip/index.js
+++ b/packages/nux/src/components/dot-tip/index.js
@@ -38,7 +38,7 @@ export function DotTip( {
focusOnMount="container"
getAnchorRect={ getAnchorRect }
role="dialog"
- aria-label={ __( 'Gutenberg tips' ) }
+ aria-label={ __( 'Editor tips' ) }
onClick={ onClick }
>
{ children }
diff --git a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap
index eb352b831c04e7..d9639628174e7d 100644
--- a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap
+++ b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap
@@ -2,7 +2,7 @@
exports[`DotTip should render correctly 1`] = `
{
- string = string.replace( /[\r\n]/g, '' );
+ // Reduce any whitespace used for HTML formatting to one space
+ // character, because it will also be displayed as such by the browser.
+ string = string.replace( /[\n\r\t]+/g, ' ' );
if ( filterString ) {
string = filterString( string );
diff --git a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap
index ce980dcd7589b7..0153822ba7ca8c 100644
--- a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap
+++ b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap
@@ -317,13 +317,6 @@ exports[`recordToDom should ignore formats at line separator 1`] = `
-
-
-
๐
@@ -339,6 +332,12 @@ exports[`recordToDom should preserve emoji in formatting 1`] = `
+ testย test
+
@@ -359,6 +358,12 @@ exports[`recordToDom should remove with settings 1`] = `
+
+
te
diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.js
index 0b4a7b6e64b704..9673619d57be2e 100644
--- a/packages/rich-text/src/test/helpers/index.js
+++ b/packages/rich-text/src/test/helpers/index.js
@@ -29,8 +29,8 @@ export const spec = [
},
},
{
- description: 'should ignore line breaks to format HTML',
- html: '\n\n\r\n',
+ description: 'should replace characters to format HTML with space',
+ html: '\n\n\r\n\t',
createRange: ( element ) => ( {
startOffset: 0,
startContainer: element,
@@ -38,12 +38,30 @@ export const spec = [
endContainer: element,
} ),
startPath: [ 0, 0 ],
- endPath: [ 0, 0 ],
+ endPath: [ 0, 1 ],
record: {
start: 0,
- end: 0,
- formats: [],
- text: '',
+ end: 1,
+ formats: [ , ],
+ text: ' ',
+ },
+ },
+ {
+ description: 'should preserve non breaking space',
+ html: 'test\u00a0 test',
+ createRange: ( element ) => ( {
+ startOffset: 5,
+ startContainer: element.firstChild,
+ endOffset: 5,
+ endContainer: element.firstChild,
+ } ),
+ startPath: [ 0, 5 ],
+ endPath: [ 0, 5 ],
+ record: {
+ start: 5,
+ end: 5,
+ formats: [ , , , , , , , , , , ],
+ text: 'test\u00a0 test',
},
},
{
diff --git a/test/e2e/specs/__snapshots__/links.test.js.snap b/test/e2e/specs/__snapshots__/links.test.js.snap
index 6b0ab2795b7af3..d63515e5d2ee34 100644
--- a/test/e2e/specs/__snapshots__/links.test.js.snap
+++ b/test/e2e/specs/__snapshots__/links.test.js.snap
@@ -20,7 +20,7 @@ exports[`Links can be created instantly when a URL is selected 1`] = `
exports[`Links can be created without any text selected 1`] = `
"
-
"
`;
diff --git a/test/e2e/specs/__snapshots__/rich-text.test.js.snap b/test/e2e/specs/__snapshots__/rich-text.test.js.snap
index e1324add1205e7..e911f407edd680 100644
--- a/test/e2e/specs/__snapshots__/rich-text.test.js.snap
+++ b/test/e2e/specs/__snapshots__/rich-text.test.js.snap
@@ -2,7 +2,7 @@
exports[`RichText should apply formatting when selection is collapsed 1`] = `
"
-
Someย bold.
+
Some bold.
"
`;
diff --git a/test/e2e/specs/__snapshots__/undo.test.js.snap b/test/e2e/specs/__snapshots__/undo.test.js.snap
index 1ae6c8e4458b79..49a8c19fe89fae 100644
--- a/test/e2e/specs/__snapshots__/undo.test.js.snap
+++ b/test/e2e/specs/__snapshots__/undo.test.js.snap
@@ -28,12 +28,12 @@ exports[`undo should undo typing after a pause 2`] = `
exports[`undo should undo typing after non input change 1`] = `
"
-
before keyboardย after keyboard
+
before keyboard afterย keyboard
"
`;
exports[`undo should undo typing after non input change 2`] = `
"
-
before keyboardย
+
before keyboard
"
`;
diff --git a/test/e2e/specs/__snapshots__/writing-flow.test.js.snap b/test/e2e/specs/__snapshots__/writing-flow.test.js.snap
index 77e8a9596b7729..a33dda228193e8 100644
--- a/test/e2e/specs/__snapshots__/writing-flow.test.js.snap
+++ b/test/e2e/specs/__snapshots__/writing-flow.test.js.snap
@@ -124,7 +124,7 @@ exports[`adding blocks should navigate around inline boundaries 1`] = `
exports[`adding blocks should not delete surrounding space when deleting a selected word 1`] = `
"
-
alphaย gamma
+
alpha gamma
"
`;
@@ -136,7 +136,7 @@ exports[`adding blocks should not delete surrounding space when deleting a selec
exports[`adding blocks should not delete surrounding space when deleting a word with Alt+Backspace 1`] = `
"
-
alphaย gamma
+
alpha gamma
"
`;
@@ -148,7 +148,7 @@ exports[`adding blocks should not delete surrounding space when deleting a word
exports[`adding blocks should not delete surrounding space when deleting a word with Backspace 1`] = `
"
-