+
diff --git a/blocks/library/gallery/index.js b/blocks/library/gallery/index.js
index 2fc8fbdf6344cf..48912f4e780fb2 100644
--- a/blocks/library/gallery/index.js
+++ b/blocks/library/gallery/index.js
@@ -7,7 +7,7 @@ import { filter, every } from 'lodash';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { createMediaFromFile } from '@wordpress/utils';
+import { createMediaFromFile, preloadImage } from '@wordpress/utils';
/**
* Internal dependencies
@@ -17,50 +17,52 @@ import './style.scss';
import { registerBlockType, createBlock } from '../../api';
import { default as GalleryBlock, defaultColumnsNumber } from './block';
+const blockAttributes = {
+ align: {
+ type: 'string',
+ default: 'none',
+ },
+ images: {
+ type: 'array',
+ default: [],
+ source: 'query',
+ selector: 'ul.wp-block-gallery .blocks-gallery-item img',
+ query: {
+ url: {
+ source: 'attribute',
+ attribute: 'src',
+ },
+ alt: {
+ source: 'attribute',
+ attribute: 'alt',
+ default: '',
+ },
+ id: {
+ source: 'attribute',
+ attribute: 'data-id',
+ },
+ },
+ },
+ columns: {
+ type: 'number',
+ },
+ imageCrop: {
+ type: 'boolean',
+ default: true,
+ },
+ linkTo: {
+ type: 'string',
+ default: 'none',
+ },
+};
+
registerBlockType( 'core/gallery', {
title: __( 'Gallery' ),
description: __( 'Image galleries are a great way to share groups of pictures on your site.' ),
icon: 'format-gallery',
category: 'common',
keywords: [ __( 'images' ), __( 'photos' ) ],
-
- attributes: {
- align: {
- type: 'string',
- default: 'none',
- },
- images: {
- type: 'array',
- default: [],
- source: 'query',
- selector: 'div.wp-block-gallery figure.blocks-gallery-image img',
- query: {
- url: {
- source: 'attribute',
- attribute: 'src',
- },
- alt: {
- source: 'attribute',
- attribute: 'alt',
- },
- id: {
- source: 'attribute',
- attribute: 'data-id',
- },
- },
- },
- columns: {
- type: 'number',
- },
- imageCrop: {
- type: 'boolean',
- default: true,
- },
- linkTo: {
- type: 'string',
- default: 'none',
- },
- },
+ attributes: blockAttributes,
transforms: {
from: [
@@ -68,8 +70,8 @@ registerBlockType( 'core/gallery', {
type: 'block',
isMultiBlock: true,
blocks: [ 'core/image' ],
- transform: ( blockAttributes ) => {
- const validImages = filter( blockAttributes, ( { id, url } ) => id && url );
+ transform: ( attributes ) => {
+ const validImages = filter( attributes, ( { id, url } ) => id && url );
if ( validImages.length > 0 ) {
return createBlock( 'core/gallery', {
images: validImages.map( ( { id, url, alt } ) => ( { id, url, alt } ) ),
@@ -113,14 +115,24 @@ registerBlockType( 'core/gallery', {
isMatch( files ) {
return files.length !== 1 && every( files, ( file ) => file.type.indexOf( 'image/' ) === 0 );
},
- transform( files ) {
- return Promise.all( files.map( ( file ) => createMediaFromFile( file ) ) )
- .then( ( medias ) => createBlock( 'core/gallery', {
- images: medias.map( media => ( {
- id: media.id,
- url: media.source_url,
- } ) ),
- } ) );
+ transform( files, onChange ) {
+ const block = createBlock( 'core/gallery', {
+ images: files.map( ( file ) => ( {
+ url: window.URL.createObjectURL( file ),
+ } ) ),
+ } );
+
+ Promise.all( files.map( ( file ) =>
+ createMediaFromFile( file )
+ .then( ( media ) => preloadImage( media.source_url ).then( () => media ) )
+ ) ).then( ( medias ) => onChange( block.uid, {
+ images: medias.map( media => ( {
+ id: media.id,
+ url: media.source_url,
+ } ) ),
+ } ) );
+
+ return block;
},
},
],
@@ -150,7 +162,7 @@ registerBlockType( 'core/gallery', {
save( { attributes } ) {
const { images, columns = defaultColumnsNumber( attributes ), align, imageCrop, linkTo } = attributes;
return (
-
+
{ images.map( ( image ) => {
let href;
@@ -166,13 +178,55 @@ registerBlockType( 'core/gallery', {
const img = ;
return (
-
- { href ? { img } : img }
-
+
+
+ { href ? { img } : img }
+
+
);
} ) }
-
+
);
},
+ deprecated: [
+ {
+ attributes: {
+ ...blockAttributes,
+ images: {
+ ...blockAttributes.images,
+ selector: 'div.wp-block-gallery figure.blocks-gallery-image img',
+ },
+ },
+
+ save( { attributes } ) {
+ const { images, columns = defaultColumnsNumber( attributes ), align, imageCrop, linkTo } = attributes;
+ return (
+
+ { images.map( ( image ) => {
+ let href;
+
+ switch ( linkTo ) {
+ case 'media':
+ href = image.url;
+ break;
+ case 'attachment':
+ href = image.link;
+ break;
+ }
+
+ const img =
;
+
+ return (
+
+ { href ? { img } : img }
+
+ );
+ } ) }
+
+ );
+ },
+ },
+ ],
+
} );
diff --git a/blocks/library/gallery/style.scss b/blocks/library/gallery/style.scss
index 7f4e436eaa2d5f..96b4f3d5081196 100644
--- a/blocks/library/gallery/style.scss
+++ b/blocks/library/gallery/style.scss
@@ -4,22 +4,31 @@
.wp-block-gallery.aligncenter {
display: flex;
flex-wrap: wrap;
+ list-style-type: none;
- .blocks-gallery-image {
+ .blocks-gallery-image,
+ .blocks-gallery-item {
margin: 8px;
display: flex;
flex-grow: 1;
flex-direction: column;
justify-content: center;
+ figure {
+ height: 100%;
+ margin: 0;
+ }
+
img {
+ display: block;
max-width: 100%;
height: auto;
}
}
// Cropped
- &.is-cropped .blocks-gallery-image {
+ &.is-cropped .blocks-gallery-image,
+ &.is-cropped .blocks-gallery-item {
img {
flex: 1;
width: 100%;
@@ -29,47 +38,29 @@
}
// Alas, IE11+ doesn't support object-fit
- _:-ms-lang(x), img {
+ _:-ms-lang(x), figure {
height: auto;
width: auto;
}
}
- &.columns-1 figure {
- width: calc(100% / 1 - 16px);
- }
- &.columns-2 figure {
- width: calc(100% / 2 - 16px);
+ // Responsive fallback value, 2 columns
+ & .blocks-gallery-image,
+ & .blocks-gallery-item {
+ width: calc( 100% / 2 - 16px );
}
- // Responsive fallback value, 2 columns
- &.columns-3 figure,
- &.columns-4 figure,
- &.columns-5 figure,
- &.columns-6 figure,
- &.columns-7 figure,
- &.columns-8 figure {
- width: calc(100% / 2 - 16px);
+ &.columns-1 .blocks-gallery-image,
+ &.columns-1 .blocks-gallery-item {
+ width: calc(100% / 1 - 16px);
}
@include break-small {
- &.columns-3 figure {
- width: calc(100% / 3 - 16px);
- }
- &.columns-4 figure {
- width: calc(100% / 4 - 16px);
- }
- &.columns-5 figure {
- width: calc(100% / 5 - 16px);
- }
- &.columns-6 figure {
- width: calc(100% / 6 - 16px);
- }
- &.columns-7 figure {
- width: calc(100% / 7 - 16px);
- }
- &.columns-8 figure {
- width: calc(100% / 8 - 16px);
+ @for $i from 3 through 8 {
+ &.columns-#{ $i } .blocks-gallery-image,
+ &.columns-#{ $i } .blocks-gallery-item {
+ width: calc(100% / #{ $i } - 16px );
+ }
}
}
}
diff --git a/blocks/library/gallery/test/__snapshots__/index.js.snap b/blocks/library/gallery/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..237a66a669cfac
--- /dev/null
+++ b/blocks/library/gallery/test/__snapshots__/index.js.snap
@@ -0,0 +1,94 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/gallery block edit matches snapshot 1`] = `
+
+
+
+ Drag images here or add from media library
+
+
+
+
+
+
+
+
+ Drop files to upload
+
+
+
+
+ *** Mock(Media upload button) ***
+
+
+`;
diff --git a/blocks/library/gallery/test/index.js b/blocks/library/gallery/test/index.js
new file mode 100644
index 00000000000000..9c6aadccd31b6d
--- /dev/null
+++ b/blocks/library/gallery/test/index.js
@@ -0,0 +1,15 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+jest.mock( 'blocks/media-upload', () => () => '*** Mock(Media upload button) ***' );
+
+describe( 'core/gallery', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/gallery' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js
index c68d85fefdbbb4..348cc2e14a2de3 100644
--- a/blocks/library/heading/index.js
+++ b/blocks/library/heading/index.js
@@ -120,7 +120,7 @@ registerBlockType( 'core/heading', {
focus && (
{ __( 'Heading Settings' ) }
- { __( 'Size' ) }
+ { __( 'Level' ) }
( {
diff --git a/blocks/library/heading/test/__snapshots__/index.js.snap b/blocks/library/heading/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..59d7b06f8c66a6
--- /dev/null
+++ b/blocks/library/heading/test/__snapshots__/index.js.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/heading block edit matches snapshot 1`] = `
+
+
+
+ Write heading…
+
+
+`;
diff --git a/blocks/library/heading/test/index.js b/blocks/library/heading/test/index.js
new file mode 100644
index 00000000000000..fa20e31292d9e0
--- /dev/null
+++ b/blocks/library/heading/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/heading', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/heading' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/html/test/__snapshots__/index.js.snap b/blocks/library/html/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..cd76f087961566
--- /dev/null
+++ b/blocks/library/html/test/__snapshots__/index.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/html block edit matches snapshot 1`] = `
+
+`;
diff --git a/blocks/library/html/test/index.js b/blocks/library/html/test/index.js
new file mode 100644
index 00000000000000..f4ef3e2b363233
--- /dev/null
+++ b/blocks/library/html/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/html', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/html' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/image/block.js b/blocks/library/image/block.js
index a7ec237664c1ce..742bccb6e468d1 100644
--- a/blocks/library/image/block.js
+++ b/blocks/library/image/block.js
@@ -17,7 +17,7 @@ import { __ } from '@wordpress/i18n';
import { Component, compose } from '@wordpress/element';
import { createMediaFromFile, getBlobByURL, revokeBlobURL, viewPort } from '@wordpress/utils';
import {
- Dashicon,
+ IconButton,
Toolbar,
withAPIData,
withContext,
@@ -28,7 +28,7 @@ import {
*/
import Editable from '../../editable';
import ImagePlaceHolder from '../../image-placeholder';
-import MediaUploadButton from '../../media-upload-button';
+import MediaUpload from '../../media-upload';
import InspectorControls from '../../inspector-controls';
import TextControl from '../../inspector-controls/text-control';
import SelectControl from '../../inspector-controls/select-control';
@@ -116,7 +116,6 @@ class ImageBlock extends Component {
const figureStyle = width ? { width } : {};
const isResizable = [ 'wide', 'full' ].indexOf( align ) === -1 && ( ! viewPort.isExtraSmall() );
- const editButtonLabel = __( 'Edit image' );
const controls = (
focus && (
@@ -126,18 +125,19 @@ class ImageBlock extends Component {
/>
-
-
-
+ render={ ( { open } ) => (
+
+ ) }
+ />
diff --git a/blocks/library/image/index.js b/blocks/library/image/index.js
index 9f3802080d404b..150c9c3f137509 100644
--- a/blocks/library/image/index.js
+++ b/blocks/library/image/index.js
@@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { createMediaFromFile } from '@wordpress/utils';
+import { createMediaFromFile, preloadImage } from '@wordpress/utils';
/**
* Internal dependencies
@@ -35,6 +35,7 @@ registerBlockType( 'core/image', {
source: 'attribute',
selector: 'img',
attribute: 'alt',
+ default: '',
},
caption: {
type: 'array',
@@ -87,12 +88,18 @@ registerBlockType( 'core/image', {
isMatch( files ) {
return files.length === 1 && files[ 0 ].type.indexOf( 'image/' ) === 0;
},
- transform( files ) {
- return createMediaFromFile( files[ 0 ] )
- .then( ( media ) => createBlock( 'core/image', {
- id: media.id,
- url: media.source_url,
- } ) );
+ transform( files, onChange ) {
+ const file = files[ 0 ];
+ const block = createBlock( 'core/image', {
+ url: window.URL.createObjectURL( file ),
+ } );
+
+ createMediaFromFile( file )
+ .then( ( media ) => preloadImage( media.source_url ).then(
+ () => onChange( block.uid, { id: media.id, url: media.source_url } )
+ ) );
+
+ return block;
},
},
{
@@ -155,9 +162,16 @@ registerBlockType( 'core/image', {
save( { attributes } ) {
const { url, alt, caption, align, href, width, height } = attributes;
const extraImageProps = width || height ? { width, height } : {};
- const figureStyle = width ? { width } : {};
const image = ;
+ let figureStyle = {};
+
+ if ( width ) {
+ figureStyle = { width };
+ } else if ( align === 'left' || align === 'right' ) {
+ figureStyle = { maxWidth: '50%' };
+ }
+
return (
{ href ? { image } : image }
diff --git a/blocks/library/index.js b/blocks/library/index.js
index 2076db1f9dc5ad..e87daedbc06731 100644
--- a/blocks/library/index.js
+++ b/blocks/library/index.js
@@ -23,3 +23,4 @@ import './video';
import './audio';
import './block';
import './paragraph';
+import './subhead';
diff --git a/blocks/library/list/index.js b/blocks/library/list/index.js
index cec6b4aa69da5d..3e538c294170d2 100644
--- a/blocks/library/list/index.js
+++ b/blocks/library/list/index.js
@@ -6,7 +6,7 @@ import { find, compact, get, initial, last, isEmpty } from 'lodash';
/**
* WordPress dependencies
*/
-import { Component, createElement, Children } from '@wordpress/element';
+import { Component, createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
@@ -17,60 +17,6 @@ import { registerBlockType, createBlock } from '../../api';
import Editable from '../../editable';
import BlockControls from '../../block-controls';
-const fromBrDelimitedContent = ( content ) => {
- if ( undefined === content ) {
- // converting an empty block to a list block
- return content;
- }
- const listItems = [];
- listItems.push( createElement( 'li', [], [] ) );
- content.forEach( function( element, elementIndex, elements ) {
- // "split" the incoming content on 'br' elements
- if ( 'br' === element.type && elementIndex < elements.length - 1 ) {
- // if is br and there are more elements to come, push a new list item
- listItems.push( createElement( 'li', [], [] ) );
- } else {
- listItems[ listItems.length - 1 ].props.children.push( element );
- }
- } );
- return listItems;
-};
-
-const toBrDelimitedContent = ( values ) => {
- if ( undefined === values ) {
- // converting an empty list
- return values;
- }
- const content = [];
- values.forEach( function( li, liIndex, listItems ) {
- if ( typeof li === 'string' ) {
- content.push( li );
- return;
- }
-
- Children.toArray( li.props.children ).forEach( function( element, elementIndex, liChildren ) {
- if ( 'ul' === element.type || 'ol' === element.type ) { // lists within lists
- // we know we've just finished processing a list item, so break the text
- content.push( createElement( 'br' ) );
- // push each element from the child list's converted content
- content.push.apply( content, toBrDelimitedContent( Children.toArray( element.props.children ) ) );
- // add a break if there are more list items to come, because the recursive call won't
- // have added it when it finished processing the child list because it thinks the content ended
- if ( liIndex !== listItems.length - 1 ) {
- content.push( createElement( 'br' ) );
- }
- } else {
- content.push( element );
- if ( elementIndex === liChildren.length - 1 && liIndex !== listItems.length - 1 ) {
- // last element in this list item, but not last element overall
- content.push( createElement( 'br' ) );
- }
- }
- } );
- } );
- return content;
-};
-
registerBlockType( 'core/list', {
title: __( 'List' ),
description: __( 'List. Numbered or bulleted.' ),
@@ -138,7 +84,7 @@ registerBlockType( 'core/list', {
transform: ( { content } ) => {
return createBlock( 'core/list', {
nodeName: 'UL',
- values: fromBrDelimitedContent( content ),
+ values: [ { content } ],
} );
},
},
@@ -148,7 +94,7 @@ registerBlockType( 'core/list', {
transform: ( { content } ) => {
return createBlock( 'core/list', {
nodeName: 'OL',
- values: fromBrDelimitedContent( content ),
+ values: [ { content } ],
} );
},
},
diff --git a/blocks/library/list/test/__snapshots__/index.js.snap b/blocks/library/list/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..b3a7b2530da6d0
--- /dev/null
+++ b/blocks/library/list/test/__snapshots__/index.js.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/list block edit matches snapshot 1`] = `
+
+`;
diff --git a/blocks/library/list/test/index.js b/blocks/library/list/test/index.js
new file mode 100644
index 00000000000000..1668762990b0a4
--- /dev/null
+++ b/blocks/library/list/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/list', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/list' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/more/test/__snapshots__/index.js.snap b/blocks/library/more/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..500ae5f763fc78
--- /dev/null
+++ b/blocks/library/more/test/__snapshots__/index.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/more block edit matches snapshot 1`] = `
+
+
+
+`;
diff --git a/blocks/library/more/test/index.js b/blocks/library/more/test/index.js
new file mode 100644
index 00000000000000..1f13406520beaa
--- /dev/null
+++ b/blocks/library/more/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/more', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/more' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/paragraph/test/__snapshots__/index.js.snap b/blocks/library/paragraph/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..fdd9ef065ba437
--- /dev/null
+++ b/blocks/library/paragraph/test/__snapshots__/index.js.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/paragraph block edit matches snapshot 1`] = `
+
+
+
+
+
+
+ Add text or type / to add content
+
+
+
+
+
+`;
diff --git a/blocks/library/paragraph/test/index.js b/blocks/library/paragraph/test/index.js
new file mode 100644
index 00000000000000..f0b7cf1659f488
--- /dev/null
+++ b/blocks/library/paragraph/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/paragraph', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/paragraph' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/preformatted/test/__snapshots__/index.js.snap b/blocks/library/preformatted/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..c950c9dab17895
--- /dev/null
+++ b/blocks/library/preformatted/test/__snapshots__/index.js.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/preformatted block edit matches snapshot 1`] = `
+
+`;
diff --git a/blocks/library/preformatted/test/index.js b/blocks/library/preformatted/test/index.js
new file mode 100644
index 00000000000000..70413a21704266
--- /dev/null
+++ b/blocks/library/preformatted/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/preformatted', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/preformatted' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/pullquote/style.scss b/blocks/library/pullquote/style.scss
index dd46d68201a172..fa750312ff6ca8 100644
--- a/blocks/library/pullquote/style.scss
+++ b/blocks/library/pullquote/style.scss
@@ -20,7 +20,8 @@
line-height: 1.6;
}
- cite {
+ cite,
+ footer {
color: $dark-gray-600;
position: relative;
font-weight: 900;
diff --git a/blocks/library/pullquote/test/__snapshots__/index.js.snap b/blocks/library/pullquote/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..dc0bb85986816f
--- /dev/null
+++ b/blocks/library/pullquote/test/__snapshots__/index.js.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/pullquote block edit matches snapshot 1`] = `
+
+
+
+`;
diff --git a/blocks/library/pullquote/test/index.js b/blocks/library/pullquote/test/index.js
new file mode 100644
index 00000000000000..74925c679092e1
--- /dev/null
+++ b/blocks/library/pullquote/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/pullquote', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/pullquote' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/quote/style.scss b/blocks/library/quote/style.scss
index 0484e6c68ce2f5..98468ea527807a 100644
--- a/blocks/library/quote/style.scss
+++ b/blocks/library/quote/style.scss
@@ -1,7 +1,8 @@
.wp-block-quote {
margin: 0 0 16px;
- cite {
+ cite,
+ footer {
color: $dark-gray-300;
margin-top: 1em;
position: relative;
@@ -17,7 +18,9 @@
font-style: italic;
line-height: 1.6;
}
- cite {
+
+ cite,
+ footer {
font-size: 19px;
text-align: right;
}
diff --git a/blocks/library/quote/test/__snapshots__/index.js.snap b/blocks/library/quote/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..9e1db3b03f7fae
--- /dev/null
+++ b/blocks/library/quote/test/__snapshots__/index.js.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/quote block edit matches snapshot 1`] = `
+
+
+
+`;
diff --git a/blocks/library/quote/test/index.js b/blocks/library/quote/test/index.js
new file mode 100644
index 00000000000000..77be56ba416bb6
--- /dev/null
+++ b/blocks/library/quote/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/quote', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/quote' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/separator/test/__snapshots__/index.js.snap b/blocks/library/separator/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..2985274d75ea3c
--- /dev/null
+++ b/blocks/library/separator/test/__snapshots__/index.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/separator block edit matches snapshot 1`] = `
+
+`;
diff --git a/blocks/library/separator/test/index.js b/blocks/library/separator/test/index.js
new file mode 100644
index 00000000000000..53efab1ac71fb0
--- /dev/null
+++ b/blocks/library/separator/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/separator', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/separator' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/shortcode/test/__snapshots__/index.js.snap b/blocks/library/shortcode/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..47439a072ac453
--- /dev/null
+++ b/blocks/library/shortcode/test/__snapshots__/index.js.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/shortcode block edit matches snapshot 1`] = `
+
+
+
+
+
+ Shortcode
+
+
+
+`;
diff --git a/blocks/library/shortcode/test/index.js b/blocks/library/shortcode/test/index.js
new file mode 100644
index 00000000000000..97e9c0b4eae7cd
--- /dev/null
+++ b/blocks/library/shortcode/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/shortcode', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/shortcode' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/subhead/editor.scss b/blocks/library/subhead/editor.scss
new file mode 100644
index 00000000000000..019eb618cd4e7a
--- /dev/null
+++ b/blocks/library/subhead/editor.scss
@@ -0,0 +1,6 @@
+// Overwrite .editor-visual-editor p
+.editor-visual-editor p.wp-block-subhead {
+ color: $dark-gray-300;
+ font-size: 1.1em;
+ font-style: italic;
+}
diff --git a/blocks/library/subhead/index.js b/blocks/library/subhead/index.js
new file mode 100644
index 00000000000000..af9f7878276236
--- /dev/null
+++ b/blocks/library/subhead/index.js
@@ -0,0 +1,91 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import './editor.scss';
+import './style.scss';
+import { registerBlockType, createBlock } from '../../api';
+import Editable from '../../editable';
+import InspectorControls from '../../inspector-controls';
+import BlockDescription from '../../block-description';
+
+registerBlockType( 'core/subhead', {
+ title: __( 'Subhead' ),
+
+ icon: 'text',
+
+ category: 'common',
+
+ useOnce: true,
+
+ attributes: {
+ content: {
+ type: 'array',
+ source: 'children',
+ selector: 'p',
+ },
+ },
+
+ transforms: {
+ from: [
+ {
+ type: 'block',
+ blocks: [ 'core/paragraph' ],
+ transform: ( { content } ) => {
+ return createBlock( 'core/subhead', {
+ content,
+ } );
+ },
+ },
+ ],
+ to: [
+ {
+ type: 'block',
+ blocks: [ 'core/paragraph' ],
+ transform: ( { content } ) => {
+ return createBlock( 'core/paragraph', {
+ content,
+ } );
+ },
+ },
+ ],
+ },
+
+ edit( { attributes, setAttributes, focus, setFocus, className } ) {
+ const { content, placeholder } = attributes;
+
+ return [
+ focus && (
+
+
+ { __( 'Explanatory text under the main heading of an article.' ) }
+
+
+ ),
+ {
+ setAttributes( {
+ content: nextContent,
+ } );
+ } }
+ focus={ focus }
+ onFocus={ setFocus }
+ className={ className }
+ placeholder={ placeholder || __( 'Write subhead…' ) }
+ />,
+ ];
+ },
+
+ save( { attributes, className } ) {
+ const { content } = attributes;
+
+ return { content }
;
+ },
+} );
diff --git a/blocks/library/subhead/style.scss b/blocks/library/subhead/style.scss
new file mode 100644
index 00000000000000..6e6f48e6bf8272
--- /dev/null
+++ b/blocks/library/subhead/style.scss
@@ -0,0 +1,5 @@
+p.wp-block-subhead {
+ font-size: 1.1em;
+ font-style: italic;
+ opacity: 0.75;
+}
diff --git a/blocks/library/table/test/__snapshots__/index.js.snap b/blocks/library/table/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..6ae6bbc45bc289
--- /dev/null
+++ b/blocks/library/table/test/__snapshots__/index.js.snap
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/embed block edit matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/blocks/library/table/test/index.js b/blocks/library/table/test/index.js
new file mode 100644
index 00000000000000..9f1456b6439a73
--- /dev/null
+++ b/blocks/library/table/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/embed', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/table' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/text-columns/test/__snapshots__/index.js.snap b/blocks/library/text-columns/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..f52ebfaf95c308
--- /dev/null
+++ b/blocks/library/text-columns/test/__snapshots__/index.js.snap
@@ -0,0 +1,44 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/text-columns block edit matches snapshot 1`] = `
+
+`;
diff --git a/blocks/library/text-columns/test/index.js b/blocks/library/text-columns/test/index.js
new file mode 100644
index 00000000000000..80abaf8e5d3343
--- /dev/null
+++ b/blocks/library/text-columns/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/text-columns', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/text-columns' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/verse/test/__snapshots__/index.js.snap b/blocks/library/verse/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..d47f3fdad11bf9
--- /dev/null
+++ b/blocks/library/verse/test/__snapshots__/index.js.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/verse block edit matches snapshot 1`] = `
+
+`;
diff --git a/blocks/library/verse/test/index.js b/blocks/library/verse/test/index.js
new file mode 100644
index 00000000000000..760898207c41c6
--- /dev/null
+++ b/blocks/library/verse/test/index.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+describe( 'core/verse', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/verse' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/library/video/index.js b/blocks/library/video/index.js
index 5c08250b6b7432..472042a42106ef 100644
--- a/blocks/library/video/index.js
+++ b/blocks/library/video/index.js
@@ -15,7 +15,7 @@ import { Component } from '@wordpress/element';
import './style.scss';
import './editor.scss';
import { registerBlockType } from '../../api';
-import MediaUploadButton from '../../media-upload-button';
+import MediaUpload from '../../media-upload';
import Editable from '../../editable';
import BlockControls from '../../block-controls';
import BlockAlignmentToolbar from '../../block-alignment-toolbar';
@@ -23,7 +23,7 @@ import BlockAlignmentToolbar from '../../block-alignment-toolbar';
registerBlockType( 'core/video', {
title: __( 'Video' ),
- description: __( 'Video, locally hosted, locally sourced.' ),
+ description: __( 'The Video block allows you to embed video files and play them back using a simple player.' ),
icon: 'format-video',
@@ -119,7 +119,7 @@ registerBlockType( 'core/video', {
key="placeholder"
icon="media-video"
label={ __( 'Video' ) }
- instructions={ __( 'Select a video file from your library, or upload a new one:' ) }
+ instructions={ __( 'Select a video file from your library, or upload a new one' ) }
className={ className }>
-
- { __( 'Add from Media Library' ) }
-
+ render={ ( { open } ) => (
+
+ { __( 'Add from Media Library' ) }
+
+ ) }
+ />
,
];
}
diff --git a/blocks/library/video/test/__snapshots__/index.js.snap b/blocks/library/video/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..fa8000f75f57c4
--- /dev/null
+++ b/blocks/library/video/test/__snapshots__/index.js.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`core/video block edit matches snapshot 1`] = `
+
+
+
+ Select a video file from your library, or upload a new one
+
+
+
+ *** Mock(Media upload button) ***
+
+
+`;
diff --git a/blocks/library/video/test/index.js b/blocks/library/video/test/index.js
new file mode 100644
index 00000000000000..3d22f432b7e4bf
--- /dev/null
+++ b/blocks/library/video/test/index.js
@@ -0,0 +1,15 @@
+/**
+ * Internal dependencies
+ */
+import '../';
+import { blockEditRender } from 'blocks/test/helpers';
+
+jest.mock( 'blocks/media-upload', () => () => '*** Mock(Media upload button) ***' );
+
+describe( 'core/video', () => {
+ test( 'block edit matches snapshot', () => {
+ const wrapper = blockEditRender( 'core/video' );
+
+ expect( wrapper ).toMatchSnapshot();
+ } );
+} );
diff --git a/blocks/media-upload/README.md b/blocks/media-upload/README.md
new file mode 100644
index 00000000000000..31330514dbcda9
--- /dev/null
+++ b/blocks/media-upload/README.md
@@ -0,0 +1,71 @@
+MediaUpload
+===========
+
+MediaUpload is a React component used to render a button that opens a the WordPress media modal.
+
+## Usage
+
+
+```jsx
+import { Button } from '@wordpress/components';
+import { MediaUpload } from '@wordpress/blocks';
+
+function MyMediaUploader() {
+ return (
+ console.log( 'selected ' + media.length ) }
+ type="image"
+ value={ mediaId }
+ render={ ( { open } ) => (
+
+ Open Media Library
+
+ ) }
+ />
+ );
+}
+```
+
+## Props
+
+The component accepts the following props. Props not included in this set will be applied to the element wrapping Popover content.
+
+### type
+
+Type of the media to upload/select from the media library (image, video, audio).
+
+- Type: `String`
+- Required: No
+
+### multiple
+
+Whether to allow multiple selections or not.
+
+- Type: `Boolean`
+- Required: No
+- Default: false
+
+### value
+
+Media ID (or media IDs if multiple is true) to be selected by default when opening the media library.
+
+- Type: `Number|Array`
+- Required: No
+
+### onSelect
+
+Callback called when the media modal is closed, the selected media are passed as an argument.
+
+- Type: `Func`
+- Required: Yes
+
+## render
+
+A callback invoked to render the Button opening the media library.
+
+- Type: `Function`
+- Required: Yes
+
+The first argument of the callback is an object containing the following properties:
+
+ - `open`: A function opening the media modal when called
diff --git a/blocks/media-upload/button.js b/blocks/media-upload/button.js
new file mode 100644
index 00000000000000..92440c3b408569
--- /dev/null
+++ b/blocks/media-upload/button.js
@@ -0,0 +1,41 @@
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+import { Button, Tooltip } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import MediaUpload from './';
+
+class MediaUploadButton extends Component {
+ componentDidMount() {
+ // eslint-disable-next-line no-console
+ console.warn( 'MediaUploadButton is deprecated use wp.blocks.MediaUpload instead' );
+ }
+
+ render() {
+ const { children, buttonProps, tooltip } = this.props;
+
+ return (
+ {
+ let element = (
+
+ { children }
+
+ );
+
+ if ( tooltip ) {
+ element = { element } ;
+ }
+
+ return element;
+ } }
+ />
+ );
+ }
+}
+
+export default MediaUploadButton;
diff --git a/blocks/media-upload-button/index.js b/blocks/media-upload/index.js
similarity index 90%
rename from blocks/media-upload-button/index.js
rename to blocks/media-upload/index.js
index 43c5ec16d16d49..f83b75fa46db0c 100644
--- a/blocks/media-upload-button/index.js
+++ b/blocks/media-upload/index.js
@@ -3,7 +3,6 @@
*/
import { Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
-import { Button, Tooltip } from '@wordpress/components';
import { pick } from 'lodash';
// Getter for the sake of unit tests.
@@ -58,7 +57,7 @@ const slimImageObject = ( img ) => {
return pick( img, attrSet );
};
-class MediaUploadButton extends Component {
+class MediaUpload extends Component {
constructor( { multiple = false, type, gallery = false, title = __( 'Select or Upload Media' ), modalClass } ) {
super( ...arguments );
this.openModal = this.openModal.bind( this );
@@ -149,20 +148,9 @@ class MediaUploadButton extends Component {
}
render() {
- const { children, buttonProps, tooltip } = this.props;
-
- let element = (
-
- { children }
-
- );
-
- if ( tooltip ) {
- element = { element } ;
- }
-
- return element;
+ return this.props.render( { open: this.openModal } );
}
}
-export default MediaUploadButton;
+export default MediaUpload;
+
diff --git a/blocks/test/fixtures/README.md b/blocks/test/fixtures/README.md
index 9be327c967bcf0..f96887d3c54ee6 100644
--- a/blocks/test/fixtures/README.md
+++ b/blocks/test/fixtures/README.md
@@ -38,7 +38,7 @@ When adding a new test, it's only necessary to create file (1) above, then
there is a command you can run to generate (2) through (4):
```sh
-GENERATE_MISSING_FIXTURES=y npm run test-unit -- --grep 'full post content fixture'
+GENERATE_MISSING_FIXTURES=y npm run test-unit blocks/test/full-content.js
```
However, when using this command, please be sure to manually verify that the
diff --git a/blocks/test/fixtures/core-embed__vine.html b/blocks/test/fixtures/core-embed__vine.html
deleted file mode 100644
index 2681598e22b3fd..00000000000000
--- a/blocks/test/fixtures/core-embed__vine.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- https://vine.com/
- Embedded content from vine
-
-
diff --git a/blocks/test/fixtures/core-embed__vine.json b/blocks/test/fixtures/core-embed__vine.json
deleted file mode 100644
index d397e79ab2a0fa..00000000000000
--- a/blocks/test/fixtures/core-embed__vine.json
+++ /dev/null
@@ -1,14 +0,0 @@
-[
- {
- "uid": "_uid_0",
- "name": "core-embed/vine",
- "isValid": true,
- "attributes": {
- "url": "https://vine.com/",
- "caption": [
- "Embedded content from vine"
- ]
- },
- "originalContent": "\n https://vine.com/\n Embedded content from vine \n "
- }
-]
diff --git a/blocks/test/fixtures/core-embed__vine.parsed.json b/blocks/test/fixtures/core-embed__vine.parsed.json
deleted file mode 100644
index 53ed119053cbcb..00000000000000
--- a/blocks/test/fixtures/core-embed__vine.parsed.json
+++ /dev/null
@@ -1,14 +0,0 @@
-[
- {
- "blockName": "core-embed/vine",
- "attrs": {
- "url": "https://vine.com/"
- },
- "innerBlocks": [],
- "innerHTML": "\n\n https://vine.com/\n Embedded content from vine \n \n"
- },
- {
- "attrs": {},
- "innerHTML": "\n"
- }
-]
diff --git a/blocks/test/fixtures/core-embed__vine.serialized.html b/blocks/test/fixtures/core-embed__vine.serialized.html
deleted file mode 100644
index 2681598e22b3fd..00000000000000
--- a/blocks/test/fixtures/core-embed__vine.serialized.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- https://vine.com/
- Embedded content from vine
-
-
diff --git a/blocks/test/fixtures/core__cover-image.json b/blocks/test/fixtures/core__cover-image.json
index 8fdfd14e7515e2..aac2689bede6d2 100644
--- a/blocks/test/fixtures/core__cover-image.json
+++ b/blocks/test/fixtures/core__cover-image.json
@@ -9,7 +9,8 @@
],
"url": "https://cldup.com/uuUqE_dXzy.jpg",
"hasParallax": false,
- "dimRatio": 40
+ "dimRatio": 40,
+ "contentAlign": "center"
},
"originalContent": ""
}
diff --git a/blocks/test/fixtures/core__gallery.html b/blocks/test/fixtures/core__gallery.html
index b74b304f41e3f3..ec923b2a360432 100644
--- a/blocks/test/fixtures/core__gallery.html
+++ b/blocks/test/fixtures/core__gallery.html
@@ -1,10 +1,14 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/blocks/test/fixtures/core__gallery.json b/blocks/test/fixtures/core__gallery.json
index 4d1777693f7532..60f85dd327fe71 100644
--- a/blocks/test/fixtures/core__gallery.json
+++ b/blocks/test/fixtures/core__gallery.json
@@ -18,6 +18,6 @@
"imageCrop": true,
"linkTo": "none"
},
- "originalContent": "\n\t
\n\t\t \n\t \n\t
\n\t\t \n\t \n
"
+ "originalContent": "\n\t\n\t\t\n\t\t\t \n\t\t \n\t \n\t\n\t\t\n\t\t\t \n\t\t \n\t \n "
}
]
diff --git a/blocks/test/fixtures/core__gallery.parsed.json b/blocks/test/fixtures/core__gallery.parsed.json
index f4c318b6e5213b..e187acdd51e60d 100644
--- a/blocks/test/fixtures/core__gallery.parsed.json
+++ b/blocks/test/fixtures/core__gallery.parsed.json
@@ -3,7 +3,7 @@
"blockName": "core/gallery",
"attrs": null,
"innerBlocks": [],
- "innerHTML": "\n\n\t
\n\t\t \n\t \n\t
\n\t\t \n\t \n
\n"
+ "innerHTML": "\n\n\t\n\t\t\n\t\t\t \n\t\t \n\t \n\t\n\t\t\n\t\t\t \n\t\t \n\t \n \n"
},
{
"attrs": {},
diff --git a/blocks/test/fixtures/core__gallery.serialized.html b/blocks/test/fixtures/core__gallery.serialized.html
index f00d97942ebef8..2e93e66f6c4ae7 100644
--- a/blocks/test/fixtures/core__gallery.serialized.html
+++ b/blocks/test/fixtures/core__gallery.serialized.html
@@ -1,6 +1,10 @@
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/blocks/test/fixtures/core__gallery__columns.html b/blocks/test/fixtures/core__gallery__columns.html
index 3fe69778320f66..24d87615b81a69 100644
--- a/blocks/test/fixtures/core__gallery__columns.html
+++ b/blocks/test/fixtures/core__gallery__columns.html
@@ -1,10 +1,14 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/blocks/test/fixtures/core__gallery__columns.json b/blocks/test/fixtures/core__gallery__columns.json
index 2b3e2e7fca6e6f..d5444c63be8a19 100644
--- a/blocks/test/fixtures/core__gallery__columns.json
+++ b/blocks/test/fixtures/core__gallery__columns.json
@@ -19,6 +19,6 @@
"imageCrop": true,
"linkTo": "none"
},
- "originalContent": "\n\t
\n\t\t \n\t \n\t
\n\t\t \n\t \n
"
+ "originalContent": "\n\t\n\t\t\n\t\t\t \n\t\t \n\t \n\t\n\t\t\n\t\t\t \n\t\t \n\t \n "
}
]
diff --git a/blocks/test/fixtures/core__gallery__columns.parsed.json b/blocks/test/fixtures/core__gallery__columns.parsed.json
index 291420ff0b4c3e..8fd100edece694 100644
--- a/blocks/test/fixtures/core__gallery__columns.parsed.json
+++ b/blocks/test/fixtures/core__gallery__columns.parsed.json
@@ -5,7 +5,7 @@
"columns": "1"
},
"innerBlocks": [],
- "innerHTML": "\n\n\t
\n\t\t \n\t \n\t
\n\t\t \n\t \n
\n"
+ "innerHTML": "\n\n\t\n\t\t\n\t\t\t \n\t\t \n\t \n\t\n\t\t\n\t\t\t \n\t\t \n\t \n \n"
},
{
"attrs": {},
diff --git a/blocks/test/fixtures/core__gallery__columns.serialized.html b/blocks/test/fixtures/core__gallery__columns.serialized.html
index 39501c3ae1a1e3..63a881085e213f 100644
--- a/blocks/test/fixtures/core__gallery__columns.serialized.html
+++ b/blocks/test/fixtures/core__gallery__columns.serialized.html
@@ -1,6 +1,10 @@
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/blocks/test/fixtures/core__image.html b/blocks/test/fixtures/core__image.html
index 2a414855b03441..eda663561a38bf 100644
--- a/blocks/test/fixtures/core__image.html
+++ b/blocks/test/fixtures/core__image.html
@@ -1,3 +1,3 @@
-
+
diff --git a/blocks/test/fixtures/core__image.json b/blocks/test/fixtures/core__image.json
index 89bdd5ba93cbc1..0aaa9b4804a968 100644
--- a/blocks/test/fixtures/core__image.json
+++ b/blocks/test/fixtures/core__image.json
@@ -5,8 +5,9 @@
"isValid": true,
"attributes": {
"url": "https://cldup.com/uuUqE_dXzy.jpg",
- "caption": []
+ "caption": [],
+ "alt": ""
},
- "originalContent": " "
+ "originalContent": " "
}
]
diff --git a/blocks/test/fixtures/core__image.parsed.json b/blocks/test/fixtures/core__image.parsed.json
index 4e50ecd68576d5..eba958b3ec9a38 100644
--- a/blocks/test/fixtures/core__image.parsed.json
+++ b/blocks/test/fixtures/core__image.parsed.json
@@ -3,7 +3,7 @@
"blockName": "core/image",
"attrs": null,
"innerBlocks": [],
- "innerHTML": "\n \n"
+ "innerHTML": "\n \n"
},
{
"attrs": {},
diff --git a/blocks/test/fixtures/core__image.serialized.html b/blocks/test/fixtures/core__image.serialized.html
index fb41212fe367e1..bbe320d2bc1404 100644
--- a/blocks/test/fixtures/core__image.serialized.html
+++ b/blocks/test/fixtures/core__image.serialized.html
@@ -1,3 +1,3 @@
-
+
diff --git a/blocks/test/fixtures/core__image__center-caption.html b/blocks/test/fixtures/core__image__center-caption.html
index 1973035e7c6e65..4fbce9c6fafd73 100644
--- a/blocks/test/fixtures/core__image__center-caption.html
+++ b/blocks/test/fixtures/core__image__center-caption.html
@@ -1,3 +1,3 @@
-Give it a try. Press the "really wide" button on the image toolbar.
+Give it a try. Press the "really wide" button on the image toolbar.
diff --git a/blocks/test/fixtures/core__image__center-caption.json b/blocks/test/fixtures/core__image__center-caption.json
index c5465d7896235a..dfd4d57389ac5f 100644
--- a/blocks/test/fixtures/core__image__center-caption.json
+++ b/blocks/test/fixtures/core__image__center-caption.json
@@ -8,8 +8,9 @@
"caption": [
"Give it a try. Press the \"really wide\" button on the image toolbar."
],
- "align": "center"
+ "align": "center",
+ "alt": ""
},
- "originalContent": "Give it a try. Press the "really wide" button on the image toolbar. "
+ "originalContent": "Give it a try. Press the "really wide" button on the image toolbar. "
}
]
diff --git a/blocks/test/fixtures/core__image__center-caption.parsed.json b/blocks/test/fixtures/core__image__center-caption.parsed.json
index b32c1f8fd1a0fe..abd5611da19486 100644
--- a/blocks/test/fixtures/core__image__center-caption.parsed.json
+++ b/blocks/test/fixtures/core__image__center-caption.parsed.json
@@ -5,7 +5,7 @@
"align": "center"
},
"innerBlocks": [],
- "innerHTML": "\nGive it a try. Press the "really wide" button on the image toolbar. \n"
+ "innerHTML": "\nGive it a try. Press the "really wide" button on the image toolbar. \n"
},
{
"attrs": {},
diff --git a/blocks/test/fixtures/core__image__center-caption.serialized.html b/blocks/test/fixtures/core__image__center-caption.serialized.html
index 3dffbf76bc8418..7b7acfcd4d09d0 100644
--- a/blocks/test/fixtures/core__image__center-caption.serialized.html
+++ b/blocks/test/fixtures/core__image__center-caption.serialized.html
@@ -1,5 +1,5 @@
-
+
Give it a try. Press the "really wide" button on the image toolbar.
diff --git a/blocks/test/fixtures/core__pullquote__multi-paragraph.json b/blocks/test/fixtures/core__pullquote__multi-paragraph.json
index 29eed74d38b3b5..2105a8afb0aecc 100644
--- a/blocks/test/fixtures/core__pullquote__multi-paragraph.json
+++ b/blocks/test/fixtures/core__pullquote__multi-paragraph.json
@@ -15,7 +15,7 @@
"Paragraph ",
{
"type": "strong",
- "key": "_domReact68",
+ "key": "_domReact67",
"ref": null,
"props": {
"children": "one"
diff --git a/blocks/test/fixtures/core__subhead.html b/blocks/test/fixtures/core__subhead.html
new file mode 100644
index 00000000000000..61cf93189617ec
--- /dev/null
+++ b/blocks/test/fixtures/core__subhead.html
@@ -0,0 +1,3 @@
+
+This is a subhead .
+
diff --git a/blocks/test/fixtures/core__subhead.json b/blocks/test/fixtures/core__subhead.json
new file mode 100644
index 00000000000000..20e5037001563c
--- /dev/null
+++ b/blocks/test/fixtures/core__subhead.json
@@ -0,0 +1,18 @@
+[
+ {
+ "uid": "_uid_0",
+ "name": "core/subhead",
+ "isValid": true,
+ "attributes": {
+ "content": [
+ "This is a ",
+ {
+ "type": "em",
+ "children": "subhead"
+ },
+ "."
+ ]
+ },
+ "originalContent": "This is a subhead .
"
+ }
+]
diff --git a/blocks/test/fixtures/core__subhead.parsed.json b/blocks/test/fixtures/core__subhead.parsed.json
new file mode 100644
index 00000000000000..a36ff7083edec0
--- /dev/null
+++ b/blocks/test/fixtures/core__subhead.parsed.json
@@ -0,0 +1,11 @@
+[
+ {
+ "blockName": "core/subhead",
+ "attrs": null,"innerBlocks": [],
+ "innerHTML": "\nThis is a subhead .
\n"
+ },
+ {
+ "attrs": {},
+ "innerHTML": "\n"
+ }
+]
diff --git a/blocks/test/fixtures/core__subhead.serialized.html b/blocks/test/fixtures/core__subhead.serialized.html
new file mode 100644
index 00000000000000..23468192081a0c
--- /dev/null
+++ b/blocks/test/fixtures/core__subhead.serialized.html
@@ -0,0 +1,3 @@
+
+This is a subhead .
+
diff --git a/blocks/test/helpers/index.js b/blocks/test/helpers/index.js
new file mode 100644
index 00000000000000..3f6dda0bfd326b
--- /dev/null
+++ b/blocks/test/helpers/index.js
@@ -0,0 +1,23 @@
+/**
+ * External dependencie
+ */
+import { render } from 'enzyme';
+import { noop } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import { createBlock, BlockEdit } from '../..';
+
+export const blockEditRender = ( name, initialAttributes = {} ) => {
+ const block = createBlock( name, initialAttributes );
+
+ return render(
+
+ );
+};
diff --git a/blocks/url-input/button.js b/blocks/url-input/button.js
index cd005b741beb47..3ec0191df113ba 100644
--- a/blocks/url-input/button.js
+++ b/blocks/url-input/button.js
@@ -43,7 +43,7 @@ class UrlInputButton extends Component {
= node in the ordering.
+ *
+ * @param {Node} node The node to find the recursive first child.
+ *
+ * @returns {Node} The first leaf-node >= node in the ordering.
*/
function descendFirst( node ) {
let n = node;
@@ -38,8 +40,10 @@ function descendFirst( node ) {
/**
* Recursively select the lastChild until hitting a leaf node.
- * @param {Node} node the node to find the recursive last child.
- * @returns {Node} the first leaf-node <= node in the ordering.
+ *
+ * @param {Node} node The node to find the recursive last child.
+ *
+ * @returns {Node} The first leaf-node <= node in the ordering.
*/
function descendLast( node ) {
let n = node;
@@ -51,8 +55,10 @@ function descendLast( node ) {
/**
* Is the node a text node.
- * @param {?Node} node the node to check.
- * @returns {boolean} true if the node is a text node.
+ *
+ * @param {?Node} node The node to check.
+ *
+ * @returns {boolean} True if the node is a text node.
*/
function isTextNode( node ) {
return node !== null && node.nodeType === 3;
@@ -60,8 +66,10 @@ function isTextNode( node ) {
/**
* Return the node only if it is a text node, otherwise return null.
- * @param {?Node} node the node to filter.
- * @returns {?Node} the node or null if it is not a text node.
+ *
+ * @param {?Node} node The node to filter.
+ *
+ * @returns {?Node} The node or null if it is not a text node.
*/
function onlyTextNode( node ) {
return isTextNode( node ) ? node : null;
@@ -69,8 +77,10 @@ function onlyTextNode( node ) {
/**
* Find the index of the last charater in the text that is whitespace.
- * @param {String} text the text to search.
- * @returns {Number} the last index of a white space character in the text or -1.
+ *
+ * @param {string} text The text to search.
+ *
+ * @returns {number} The last index of a white space character in the text or -1.
*/
function lastIndexOfSpace( text ) {
for ( let i = text.length - 1; i >= 0; i-- ) {
diff --git a/components/autocomplete/test/index.js b/components/autocomplete/test/index.js
index cb6ab7db9cf407..9b71e1258fba41 100644
--- a/components/autocomplete/test/index.js
+++ b/components/autocomplete/test/index.js
@@ -53,17 +53,20 @@ function makeAutocompleter( completers, AutocompleteComponent = Autocomplete ) {
}
/**
- * Create a text node
- * @param {String} text text of text node.
- * @returns {Node} a text node.
+ * Create a text node.
+ *
+ * @param {string} text Text of text node.
+
+ * @returns {Node} A text node.
*/
function tx( text ) {
return document.createTextNode( text );
}
/**
- * Create a paragraph node with the arguments as children
- * @returns {Node} a paragraph node.
+ * Create a paragraph node with the arguments as children.
+
+ * @returns {Node} A paragraph node.
*/
function par( /* arguments */ ) {
const p = document.createElement( 'p' );
@@ -75,9 +78,12 @@ function par( /* arguments */ ) {
* Simulate typing into the fake editor by updating the content and simulating
* an input event. It also updates the data-cursor attribute which is used to
* simulate the cursor position in the test mocks.
- * @param {*} wrapper enzyme wrapper around react node containing a FakeEditor.
- * @param {Array.} nodeList array of dom nodes.
- * @param {Array.} cursorPosition array specifying the child indexes and offset of the cursor
+ *
+ * @param {*} wrapper Enzyme wrapper around react node
+ * containing a FakeEditor.
+ * @param {Array.} nodeList Array of dom nodes.
+ * @param {Array.} cursorPosition Array specifying the child indexes and
+ * offset of the cursor.
*/
function simulateInput( wrapper, nodeList, cursorPosition ) {
// update the editor content
@@ -98,8 +104,10 @@ function simulateInput( wrapper, nodeList, cursorPosition ) {
/**
* Fire a native keydown event on the fake editor in the wrapper.
- * @param {*} wrapper the wrapper containing the FakeEditor where the event will be dispatched.
- * @param {*} keyCode the keycode of the key event.
+ *
+ * @param {*} wrapper The wrapper containing the FakeEditor where the event will
+ * be dispatched.
+ * @param {*} keyCode The keycode of the key event.
*/
function simulateKeydown( wrapper, keyCode ) {
const fakeEditor = wrapper.getDOMNode().querySelector( '.fake-editor' );
@@ -110,7 +118,8 @@ function simulateKeydown( wrapper, keyCode ) {
/**
* Check that the autocomplete matches the initial state.
- * @param {*} wrapper the enzyme react wrapper.
+ *
+ * @param {*} wrapper The enzyme react wrapper.
*/
function expectInitialState( wrapper ) {
expect( wrapper.state( 'open' ) ).toBeUndefined();
diff --git a/components/clipboard-button/index.js b/components/clipboard-button/index.js
index 3c4a78f30783a0..79f550acc60cb6 100644
--- a/components/clipboard-button/index.js
+++ b/components/clipboard-button/index.js
@@ -38,6 +38,7 @@ class ClipboardButton extends Component {
componentWillUnmount() {
this.clipboard.destroy();
delete this.clipboard;
+ clearTimeout( this.onCopyTimeout );
}
bindContainer( container ) {
@@ -50,9 +51,17 @@ class ClipboardButton extends Component {
// kept within the rendered node.
args.clearSelection();
- const { onCopy } = this.props;
+ const { onCopy, onFinishCopy } = this.props;
if ( onCopy ) {
onCopy();
+ // For convenience and consistency, ClipboardButton offers to call
+ // a secondary callback with delay. This is useful to reset
+ // consumers' state, e.g. to revert a label from "Copied" to
+ // "Copy".
+ if ( onFinishCopy ) {
+ clearTimeout( this.onCopyTimeout );
+ this.onCopyTimeout = setTimeout( onFinishCopy, 4000 );
+ }
}
}
@@ -68,7 +77,7 @@ class ClipboardButton extends Component {
render() {
// Disable reason: Exclude from spread props passed to Button
// eslint-disable-next-line no-unused-vars
- const { className, children, onCopy, text, ...buttonProps } = this.props;
+ const { className, children, onCopy, onFinishCopy, text, ...buttonProps } = this.props;
const classes = classnames( 'components-clipboard-button', className );
return (
diff --git a/components/drop-zone/index.js b/components/drop-zone/index.js
index 24954fea1605c0..1d439572eb5dd5 100644
--- a/components/drop-zone/index.js
+++ b/components/drop-zone/index.js
@@ -23,6 +23,7 @@ class DropZone extends Component {
this.setZoneNode = this.setZoneNode.bind( this );
this.onDrop = this.onDrop.bind( this );
this.onFilesDrop = this.onFilesDrop.bind( this );
+ this.onHTMLDrop = this.onHTMLDrop.bind( this );
this.state = {
isDraggingOverDocument: false,
@@ -37,6 +38,7 @@ class DropZone extends Component {
updateState: this.setState.bind( this ),
onDrop: this.onDrop,
onFilesDrop: this.onFilesDrop,
+ onHTMLDrop: this.onHTMLDrop,
} );
}
@@ -56,6 +58,12 @@ class DropZone extends Component {
}
}
+ onHTMLDrop() {
+ if ( this.props.onHTMLDrop ) {
+ this.props.onHTMLDrop( ...arguments );
+ }
+ }
+
setZoneNode( node ) {
this.zone = node;
}
diff --git a/components/drop-zone/provider.js b/components/drop-zone/provider.js
index 628f2cc0e13d36..d9b2e28ff0a194 100644
--- a/components/drop-zone/provider.js
+++ b/components/drop-zone/provider.js
@@ -29,8 +29,8 @@ class DropZoneProvider extends Component {
getChildContext() {
return {
dropzones: {
- add: ( { element, updateState, onDrop, onFilesDrop } ) => {
- this.dropzones.push( { element, updateState, onDrop, onFilesDrop } );
+ add: ( { element, updateState, onDrop, onFilesDrop, onHTMLDrop } ) => {
+ this.dropzones.push( { element, updateState, onDrop, onFilesDrop, onHTMLDrop } );
},
remove: ( element ) => {
this.dropzones = filter( this.dropzones, ( dropzone ) => dropzone.element !== element );
@@ -174,8 +174,15 @@ class DropZoneProvider extends Component {
return;
}
- if ( event.dataTransfer && !! dropzone && !! dropzone.onFilesDrop ) {
- dropzone.onFilesDrop( Array.prototype.slice.call( event.dataTransfer.files ), position );
+ if ( event.dataTransfer && !! dropzone ) {
+ const files = event.dataTransfer.files;
+ const HTML = event.dataTransfer.getData( 'text/html' );
+
+ if ( files.length && dropzone.onFilesDrop ) {
+ dropzone.onFilesDrop( [ ...event.dataTransfer.files ], position );
+ } else if ( HTML && dropzone.onHTMLDrop ) {
+ dropzone.onHTMLDrop( HTML, position );
+ }
}
event.stopPropagation();
diff --git a/components/higher-order/with-api-data/index.js b/components/higher-order/with-api-data/index.js
index 36c95165c1ea85..19c4e4910c19f4 100644
--- a/components/higher-order/with-api-data/index.js
+++ b/components/higher-order/with-api-data/index.js
@@ -139,16 +139,24 @@ export default ( mapPropsToData ) => ( WrappedComponent ) => {
[ this.getPendingKey( method ) ]: true,
} );
- request( { path, method } ).then( ( response ) => {
- this.setIntoDataProp( propName, {
- [ this.getPendingKey( method ) ]: false,
+ request( { path, method } )
+ // [Success] Set the data prop:
+ .then( ( response ) => ( {
[ this.getResponseDataKey( method ) ]: response.body,
- } );
- } ).catch( ( error ) => {
- this.setIntoDataProp( propName, {
+ } ) )
+
+ // [Failure] Set the error prop:
+ .catch( ( error ) => ( {
[ this.getErrorResponseKey( method ) ]: error,
+ } ) )
+
+ // Always reset loading prop:
+ .then( ( nextDataProp ) => {
+ this.setIntoDataProp( propName, {
+ [ this.getPendingKey( method ) ]: false,
+ ...nextDataProp,
+ } );
} );
- } );
}
applyMapping( props ) {
diff --git a/components/higher-order/with-api-data/request.js b/components/higher-order/with-api-data/request.js
index 627fbcba5c16a5..d28a556a33952b 100644
--- a/components/higher-order/with-api-data/request.js
+++ b/components/higher-order/with-api-data/request.js
@@ -45,8 +45,9 @@ export const cache = mapKeys(
*
* @see https://xhr.spec.whatwg.org/#the-getallresponseheaders()-method
*
- * @param {XMLHttpRequest} xhr XMLHttpRequest object
- * @return {Array[]} Array of header tuples
+ * @param {XMLHttpRequest} xhr XMLHttpRequest object.
+ *
+ * @returns {Array[]} Array of header tuples.
*/
export function getResponseHeaders( xhr ) {
// 'date: Tue, 22 Aug 2017 18:45:28 GMT↵server: nginx'
@@ -63,8 +64,9 @@ export function getResponseHeaders( xhr ) {
* Returns a response payload if GET request and a cached result exists, or
* undefined otherwise.
*
- * @param {Object} request Request object (path, method)
- * @return {?Object} Response object (body, headers)
+ * @param {Object} request Request object (path, method).
+ *
+ * @returns {?Object} Response object (body, headers).
*/
export function getCachedResponse( request ) {
if ( isRequestMethod( request, 'GET' ) ) {
diff --git a/components/higher-order/with-api-data/routes.js b/components/higher-order/with-api-data/routes.js
index 752cb54ad68f2a..0d51219c9cb076 100644
--- a/components/higher-order/with-api-data/routes.js
+++ b/components/higher-order/with-api-data/routes.js
@@ -20,8 +20,9 @@ const RE_NAMED_SUBPATTERN = /\(\?P?[<']\w+[>'](.*?)\)/g;
* replacing named subpatterns (unsupported in JavaScript), allowing trailing
* slash, allowing query parameters, but otherwise enforcing strict equality.
*
- * @param {String} pattern PCRE regular expression string
- * @return {RegExp} Equivalent JavaScript RegExp
+ * @param {string} pattern PCRE regular expression string.
+ *
+ * @returns {RegExp} Equivalent JavaScript RegExp.
*/
export function getNormalizedRegExp( pattern ) {
pattern = pattern.replace( RE_NAMED_SUBPATTERN, '($1)' );
@@ -32,9 +33,10 @@ export function getNormalizedRegExp( pattern ) {
/**
* Returns true if the route path pattern string matches the given path.
*
- * @param {String} pattern PCRE route path pattern
- * @param {String} path URL path
- * @return {Boolean} Whether path is a match
+ * @param {string} pattern PCRE route path pattern.
+ * @param {string} path URL path.
+ *
+ * @returns {boolean} Whether path is a match.
*/
export function isRouteMatch( pattern, path ) {
return getNormalizedRegExp( pattern ).test( path );
@@ -43,9 +45,10 @@ export function isRouteMatch( pattern, path ) {
/**
* Returns a REST route object for a given path, if one exists.
*
- * @param {Object} schema REST schema
- * @param {String} path URL path
- * @return {?Object} REST route
+ * @param {Object} schema REST schema.
+ * @param {string} path URL path.
+ *
+ * @returns {?Object} REST route.
*/
export const getRoute = createSelector( ( schema, path ) => {
return find( schema.routes, ( route, pattern ) => {
diff --git a/components/higher-order/with-api-data/test/index.js b/components/higher-order/with-api-data/test/index.js
index cd42a1cd82fcf1..e6036ef99d81f4 100644
--- a/components/higher-order/with-api-data/test/index.js
+++ b/components/higher-order/with-api-data/test/index.js
@@ -10,9 +10,17 @@ import { identity, fromPairs } from 'lodash';
import withAPIData from '../';
jest.mock( '../request', () => {
- const request = jest.fn( () => Promise.resolve( {
- body: {},
- } ) );
+ const request = jest.fn( ( { path } ) => {
+ if ( /\/users$/.test( path ) ) {
+ return Promise.reject( {
+ code: 'rest_forbidden_context',
+ message: 'Sorry, you are not allowed to list users.',
+ data: { status: 403 },
+ } );
+ }
+
+ return Promise.resolve( { body: {} } );
+ } );
request.getCachedResponse = ( { method, path } ) => {
return method === 'GET' && '/wp/v2/pages/10' === path ?
@@ -29,6 +37,9 @@ describe( 'withAPIData()', () => {
'/wp/v2/pages/(?P[\\d]+)/revisions': {
methods: [ 'GET' ],
},
+ '/wp/v2/users': {
+ methods: [ 'GET' ],
+ },
'/wp/v2/pages/(?P[\\d]+)': {
methods: [
'GET',
@@ -80,6 +91,20 @@ describe( 'withAPIData()', () => {
} );
} );
+ it( 'should handle error response', ( done ) => {
+ const wrapper = getWrapper( () => ( {
+ users: '/wp/v2/users',
+ } ) );
+
+ process.nextTick( () => {
+ expect( wrapper.state( 'dataProps' ).users.isLoading ).toBe( false );
+ expect( wrapper.state( 'dataProps' ).users ).not.toHaveProperty( 'data' );
+ expect( wrapper.state( 'dataProps' ).users.error.code ).toBe( 'rest_forbidden_context' );
+
+ done();
+ } );
+ } );
+
it( 'should preassign cached data', ( done ) => {
const wrapper = getWrapper( () => ( {
page: '/wp/v2/pages/10',
diff --git a/components/higher-order/with-filters/index.js b/components/higher-order/with-filters/index.js
index 34eeb87340542c..883d683574dfa2 100644
--- a/components/higher-order/with-filters/index.js
+++ b/components/higher-order/with-filters/index.js
@@ -1,24 +1,64 @@
+/**
+ * External dependencies
+ */
+import { debounce, uniqueId } from 'lodash';
+
/**
* WordPress dependencies
*/
import { Component, getWrapperDisplayName } from '@wordpress/element';
-import { applyFilters } from '@wordpress/hooks';
+import { addAction, applyFilters, removeAction } from '@wordpress/hooks';
+
+const ANIMATION_FRAME_PERIOD = 16;
/**
- * Creates a higher-order component which adds filtering capability to the wrapped component.
- * Filters get applied when the original component is about to be mounted.
+ * Creates a higher-order component which adds filtering capability to the
+ * wrapped component. Filters get applied when the original component is about
+ * to be mounted. When a filter is added or removed that matches the hook name,
+ * the wrapped component re-renders.
+ *
+ * @param {string} hookName Hook name exposed to be used by filters.
*
- * @param {String} hookName Hook name exposed to be used by filters.
- * @return {Function} Higher-order component factory.
+ * @returns {Function} Higher-order component factory.
*/
export default function withFilters( hookName ) {
return ( OriginalComponent ) => {
class FilteredComponent extends Component {
+ /** @inheritdoc */
constructor( props ) {
super( props );
+
+ this.onHooksUpdated = this.onHooksUpdated.bind( this );
this.Component = applyFilters( hookName, OriginalComponent );
+ this.namespace = uniqueId( 'core/with-filters/component-' );
+ this.throttledForceUpdate = debounce( () => {
+ this.Component = applyFilters( hookName, OriginalComponent );
+ this.forceUpdate();
+ }, ANIMATION_FRAME_PERIOD );
+
+ addAction( 'hookRemoved', this.namespace, this.onHooksUpdated );
+ addAction( 'hookAdded', this.namespace, this.onHooksUpdated );
+ }
+
+ /** @inheritdoc */
+ componentWillUnmount() {
+ this.throttledForceUpdate.cancel();
+ removeAction( 'hookRemoved', this.namespace );
+ removeAction( 'hookAdded', this.namespace );
+ }
+
+ /**
+ * When a filter is added or removed for the matching hook name, the wrapped component should re-render.
+ *
+ * @param {string} updatedHookName Name of the hook that was updated.
+ */
+ onHooksUpdated( updatedHookName ) {
+ if ( updatedHookName === hookName ) {
+ this.throttledForceUpdate();
+ }
}
+ /** @inheritdoc */
render() {
return ;
}
diff --git a/components/higher-order/with-filters/test/index.js b/components/higher-order/with-filters/test/index.js
index c7931c43d593f9..2c01d0ecf9883e 100644
--- a/components/higher-order/with-filters/test/index.js
+++ b/components/higher-order/with-filters/test/index.js
@@ -1,29 +1,33 @@
/**
* External dependencies
*/
-import { shallow } from 'enzyme';
+import { mount, shallow } from 'enzyme';
/**
* WordPress dependencies
*/
-import { addFilter, removeAllFilters } from '@wordpress/hooks';
+import { addFilter, removeAllFilters, removeFilter } from '@wordpress/hooks';
/**
* Internal dependencies
*/
-import withFilters from '../';
+import withFilters from '..';
describe( 'withFilters', () => {
+ let wrapper;
+
const hookName = 'EnhancedComponent';
const MyComponent = () => My component
;
afterEach( () => {
+ wrapper.unmount();
removeAllFilters( hookName );
} );
it( 'should display original component when no filters applied', () => {
const EnhancedComponent = withFilters( hookName )( MyComponent );
- const wrapper = shallow( );
+
+ wrapper = shallow( );
expect( wrapper.html() ).toBe( 'My component
' );
} );
@@ -37,7 +41,7 @@ describe( 'withFilters', () => {
);
const EnhancedComponent = withFilters( hookName )( MyComponent );
- const wrapper = shallow( );
+ wrapper = shallow( );
expect( wrapper.html() ).toBe( 'Overridden component
' );
} );
@@ -56,8 +60,131 @@ describe( 'withFilters', () => {
);
const EnhancedComponent = withFilters( hookName )( MyComponent );
- const wrapper = shallow( );
+ wrapper = shallow( );
expect( wrapper.html() ).toBe( 'My component
Composed component
' );
} );
+
+ it( 'should re-render component once when new filter added after component was mounted', () => {
+ const spy = jest.fn();
+ const SpiedComponent = () => {
+ spy();
+ return Spied component
;
+ };
+ const EnhancedComponent = withFilters( hookName )( SpiedComponent );
+
+ wrapper = mount( );
+
+ spy.mockClear();
+ addFilter(
+ hookName,
+ 'test/enhanced-component-spy-1',
+ FilteredComponent => () => (
+
+
+
+ ),
+ );
+ jest.runAllTimers();
+
+ expect( spy ).toHaveBeenCalledTimes( 1 );
+ expect( wrapper.html() ).toBe( 'Spied component
' );
+ } );
+
+ it( 'should re-render component once when two filters added in the same animation frame', () => {
+ const spy = jest.fn();
+ const SpiedComponent = () => {
+ spy();
+ return Spied component
;
+ };
+ const EnhancedComponent = withFilters( hookName )( SpiedComponent );
+ wrapper = mount( );
+
+ spy.mockClear();
+
+ addFilter(
+ hookName,
+ 'test/enhanced-component-spy-1',
+ FilteredComponent => () => (
+
+
+
+ ),
+ );
+ addFilter(
+ hookName,
+ 'test/enhanced-component-spy-2',
+ FilteredComponent => () => (
+
+ ),
+ );
+ jest.runAllTimers();
+
+ expect( spy ).toHaveBeenCalledTimes( 1 );
+ expect( wrapper.html() ).toBe( '' );
+ } );
+
+ it( 'should re-render component twice when new filter added and removed in two different animation frames', () => {
+ const spy = jest.fn();
+ const SpiedComponent = () => {
+ spy();
+ return Spied component
;
+ };
+ const EnhancedComponent = withFilters( hookName )( SpiedComponent );
+ wrapper = mount( );
+
+ spy.mockClear();
+ addFilter(
+ hookName,
+ 'test/enhanced-component-spy',
+ FilteredComponent => () => (
+
+
+
+ ),
+ );
+ jest.runAllTimers();
+
+ removeFilter(
+ hookName,
+ 'test/enhanced-component-spy',
+ );
+ jest.runAllTimers();
+
+ expect( spy ).toHaveBeenCalledTimes( 2 );
+ expect( wrapper.html() ).toBe( 'Spied component
' );
+ } );
+
+ it( 'should re-render both components once each when one filter added', () => {
+ const spy = jest.fn();
+ const SpiedComponent = () => {
+ spy();
+ return Spied component
;
+ };
+ const EnhancedComponent = withFilters( hookName )( SpiedComponent );
+ const CombinedComponents = () => (
+
+
+
+
+ );
+ wrapper = mount( );
+
+ spy.mockClear();
+ addFilter(
+ hookName,
+ 'test/enhanced-component-spy-1',
+ FilteredComponent => () => (
+
+
+
+ ),
+ );
+ jest.runAllTimers();
+
+ expect( spy ).toHaveBeenCalledTimes( 2 );
+ expect( wrapper.html() ).toBe( 'Spied component
Spied component
' );
+ } );
} );
diff --git a/components/higher-order/with-focus-return/index.js b/components/higher-order/with-focus-return/index.js
index 615205c75d2474..dd521ca7c702e7 100644
--- a/components/higher-order/with-focus-return/index.js
+++ b/components/higher-order/with-focus-return/index.js
@@ -4,13 +4,14 @@
import { Component } from '@wordpress/element';
/**
- * Higher Order Component used to be used to wrap disposable elements like Sidebars, modals, dropdowns.
- * When mounting the wrapped component, we track a reference to the current active element
- * so we know where to restore focus when the component is unmounted
+ * Higher Order Component used to be used to wrap disposable elements like
+ * sidebars, modals, dropdowns. When mounting the wrapped component, we track a
+ * reference to the current active element so we know where to restore focus
+ * when the component is unmounted.
*
- * @param {WPElement} WrappedComponent The disposable component
+ * @param {WPElement} WrappedComponent The disposable component.
*
- * @return {Component} Component with the focus restauration behaviour
+ * @returns {Component} Component with the focus restauration behaviour.
*/
function withFocusReturn( WrappedComponent ) {
return class extends Component {
diff --git a/components/higher-order/with-instance-id/index.js b/components/higher-order/with-instance-id/index.js
index 400e891f6113d7..d35cb5cc0b3079 100644
--- a/components/higher-order/with-instance-id/index.js
+++ b/components/higher-order/with-instance-id/index.js
@@ -4,11 +4,12 @@
import { Component } from '@wordpress/element';
/**
- * A Higher Order Component used to be provide a unique instance ID by component
+ * A Higher Order Component used to be provide a unique instance ID by
+ * component.
*
- * @param {WPElement} WrappedComponent The wrapped component
+ * @param {WPElement} WrappedComponent The wrapped component.
*
- * @return {Component} Component with an instanceId prop.
+ * @returns {Component} Component with an instanceId prop.
*/
function withInstanceId( WrappedComponent ) {
let instances = 0;
diff --git a/components/higher-order/with-spoken-messages/index.js b/components/higher-order/with-spoken-messages/index.js
index 6b5895983017d7..4c16e216df2fd2 100644
--- a/components/higher-order/with-spoken-messages/index.js
+++ b/components/higher-order/with-spoken-messages/index.js
@@ -10,11 +10,12 @@ import { Component } from '@wordpress/element';
import { speak } from '@wordpress/a11y';
/**
- * A Higher Order Component used to be provide a unique instance ID by component
+ * A Higher Order Component used to be provide a unique instance ID by
+ * component.
*
- * @param {WPElement} WrappedComponent The wrapped component
+ * @param {WPElement} WrappedComponent The wrapped component.
*
- * @return {Component} Component with an instanceId prop.
+ * @returns {Component} Component with an instanceId prop.
*/
function withSpokenMessages( WrappedComponent ) {
return class extends Component {
diff --git a/components/higher-order/with-state/index.js b/components/higher-order/with-state/index.js
index 3e2bea08a32da6..9e7595f357cc59 100644
--- a/components/higher-order/with-state/index.js
+++ b/components/higher-order/with-state/index.js
@@ -7,8 +7,9 @@ import { Component, getWrapperDisplayName } from '@wordpress/element';
* A Higher Order Component used to provide and manage internal component state
* via props.
*
- * @param {?Object} initialState Optional initial state of the component
- * @return {Component} Wrapped component
+ * @param {?Object} initialState Optional initial state of the component.
+ *
+ * @returns {Component} Wrapped component.
*/
function withState( initialState = {} ) {
return ( OriginalComponent ) => {
diff --git a/components/icon-button/style.scss b/components/icon-button/style.scss
index 71b1f1f5851baf..66c13a02c53d4f 100644
--- a/components/icon-button/style.scss
+++ b/components/icon-button/style.scss
@@ -22,4 +22,14 @@
&:not( :disabled ):hover {
@include button-style__hover;
}
+
+ &:not( :disabled ):active {
+ @include button-style__active;
+ }
+
+ &[aria-disabled=true]:focus,
+ &:disabled:focus {
+ box-shadow: none;
+ }
+
}
diff --git a/components/menu-items/menu-items-group.js b/components/menu-items/menu-items-group.js
index d59f9785b2b097..27a8af4533361d 100644
--- a/components/menu-items/menu-items-group.js
+++ b/components/menu-items/menu-items-group.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
/**
* Internal dependencies
*/
@@ -6,11 +11,23 @@ import { NavigableMenu } from '../navigable-container';
import withInstanceId from '../higher-order/with-instance-id';
import MenuItemsToggle from './menu-items-toggle';
-function MenuItemsGroup( { label, value, choices = [], onSelect, children, instanceId } ) {
+function MenuItemsGroup( {
+ label,
+ value,
+ choices = [],
+ onSelect,
+ children,
+ instanceId,
+ className = '',
+} ) {
const labelId = `components-choice-menu-label-${ instanceId }`;
+ const classNames = classnames( className, 'components-choice-menu' );
+
return (
-
-
{ label }
+
+ { label &&
+
{ label }
+ }
{ choices.map( ( item ) => {
const isSelected = value === item.value;
diff --git a/components/menu-items/menu-items-toggle.js b/components/menu-items/menu-items-toggle.js
index 3d70f4316e4efc..e7b4e97a22f51f 100644
--- a/components/menu-items/menu-items-toggle.js
+++ b/components/menu-items/menu-items-toggle.js
@@ -10,7 +10,7 @@ function MenuItemsToggle( { label, isSelected, onClick, shortcut } ) {
if ( isSelected ) {
return (
@@ -22,7 +22,7 @@ function MenuItemsToggle( { label, isSelected, onClick, shortcut } ) {
return (
{ label }
diff --git a/components/menu-items/style.scss b/components/menu-items/style.scss
index 3ea31a987bc25d..197ac9fde282d7 100644
--- a/components/menu-items/style.scss
+++ b/components/menu-items/style.scss
@@ -8,8 +8,8 @@
color: $dark-gray-300;
}
-.components-menu-items__toggle,
-.components-menu-items__toggle.components-icon-button {
+.components-menu-items__button,
+.components-menu-items__button.components-icon-button {
width: 100%;
padding: 8px;
text-align: left;
diff --git a/components/slot-fill/fill.js b/components/slot-fill/fill.js
index 628e434bbf27d3..dd32649554ec9b 100644
--- a/components/slot-fill/fill.js
+++ b/components/slot-fill/fill.js
@@ -25,6 +25,11 @@ class Fill extends Component {
if ( ! this.occurrence ) {
this.occurrence = ++occurrences;
}
+ const { getSlot = noop } = this.context;
+ const slot = getSlot( this.props.name );
+ if ( slot && ! slot.props.bubblesVirtually ) {
+ slot.forceUpdate();
+ }
}
componentWillUnmount() {
@@ -46,14 +51,6 @@ class Fill extends Component {
}
}
- componentDidUpdate() {
- const { getSlot = noop } = this.context;
- const slot = getSlot( this.props.name );
- if ( slot && ! slot.props.bubblesVirtually ) {
- slot.forceUpdate();
- }
- }
-
resetOccurrence() {
this.occurrence = null;
}
diff --git a/components/tooltip/index.js b/components/tooltip/index.js
index 89deb58a12b46b..4ec47c248661df 100644
--- a/components/tooltip/index.js
+++ b/components/tooltip/index.js
@@ -59,9 +59,9 @@ class Tooltip extends Component {
}
/**
- * Assigns DOM node of the rendered component as an instance property
+ * Assigns DOM node of the rendered component as an instance property.
*
- * @param {Element} ref Rendered component reference
+ * @param {Element} ref Rendered component reference.
*/
bindNode( ref ) {
// Disable reason: Because render clones the child, we don't know what
@@ -72,7 +72,7 @@ class Tooltip extends Component {
}
/**
- * Disconnects any DOM observer attached to the rendered node
+ * Disconnects any DOM observer attached to the rendered node.
*/
disconnectDisabledAttributeObserver() {
if ( this.observer ) {
@@ -82,7 +82,7 @@ class Tooltip extends Component {
/**
* Adds a DOM observer to the rendered node, if supported and if the DOM
- * node exists, to monitor for application of a disabled attribute
+ * node exists, to monitor for application of a disabled attribute.
*/
observeDisabledAttribute() {
if ( ! window.MutationObserver || ! this.node ) {
diff --git a/composer.json b/composer.json
index 20989e187af8bf..1c35db62abd5a1 100644
--- a/composer.json
+++ b/composer.json
@@ -18,5 +18,8 @@
},
"require": {
"composer/installers": "~1.0"
+ },
+ "scripts": {
+ "lint": "phpcs"
}
}
diff --git a/composer.lock b/composer.lock
index 0971f6bb68e7d1..04532b14b50dae 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1,347 +1,347 @@
{
- "_readme": [
- "This file locks the dependencies of your project to a known state",
- "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
- "This file is @generated automatically"
- ],
- "content-hash": "6294e908cbc6a7c0e62e92f44e7a428c",
- "packages": [
- {
- "name": "composer/installers",
- "version": "v1.4.0",
- "source": {
- "type": "git",
- "url": "https://github.com/composer/installers.git",
- "reference": "9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/composer/installers/zipball/9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b",
- "reference": "9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b",
- "shasum": ""
- },
- "require": {
- "composer-plugin-api": "^1.0"
- },
- "replace": {
- "roundcube/plugin-installer": "*",
- "shama/baton": "*"
- },
- "require-dev": {
- "composer/composer": "1.0.*@dev",
- "phpunit/phpunit": "4.1.*"
- },
- "type": "composer-plugin",
- "extra": {
- "class": "Composer\\Installers\\Plugin",
- "branch-alias": {
- "dev-master": "1.0-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Composer\\Installers\\": "src/Composer/Installers"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Kyle Robinson Young",
- "email": "kyle@dontkry.com",
- "homepage": "https://github.com/shama"
- }
- ],
- "description": "A multi-framework Composer library installer",
- "homepage": "https://composer.github.io/installers/",
- "keywords": [
- "Craft",
- "Dolibarr",
- "Eliasis",
- "Hurad",
- "ImageCMS",
- "Kanboard",
- "Lan Management System",
- "MODX Evo",
- "Mautic",
- "Maya",
- "OXID",
- "Plentymarkets",
- "Porto",
- "RadPHP",
- "SMF",
- "Thelia",
- "WolfCMS",
- "agl",
- "aimeos",
- "annotatecms",
- "attogram",
- "bitrix",
- "cakephp",
- "chef",
- "cockpit",
- "codeigniter",
- "concrete5",
- "croogo",
- "dokuwiki",
- "drupal",
- "eZ Platform",
- "elgg",
- "expressionengine",
- "fuelphp",
- "grav",
- "installer",
- "itop",
- "joomla",
- "kohana",
- "laravel",
- "lavalite",
- "lithium",
- "magento",
- "mako",
- "mediawiki",
- "modulework",
- "moodle",
- "osclass",
- "phpbb",
- "piwik",
- "ppi",
- "puppet",
- "reindex",
- "roundcube",
- "shopware",
- "silverstripe",
- "sydes",
- "symfony",
- "typo3",
- "wordpress",
- "yawik",
- "zend",
- "zikula"
- ],
- "time": "2017-08-09T07:53:48+00:00"
- }
- ],
- "packages-dev": [
- {
- "name": "dealerdirect/phpcodesniffer-composer-installer",
- "version": "v0.4.3",
- "source": {
- "type": "git",
- "url": "https://github.com/DealerDirect/phpcodesniffer-composer-installer.git",
- "reference": "63c0ec0ac286d31651d3c70e5bf76ad87db3ba23"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/DealerDirect/phpcodesniffer-composer-installer/zipball/63c0ec0ac286d31651d3c70e5bf76ad87db3ba23",
- "reference": "63c0ec0ac286d31651d3c70e5bf76ad87db3ba23",
- "shasum": ""
- },
- "require": {
- "composer-plugin-api": "^1.0",
- "php": "^5.3|^7",
- "squizlabs/php_codesniffer": "*"
- },
- "require-dev": {
- "composer/composer": "*",
- "wimg/php-compatibility": "^8.0"
- },
- "suggest": {
- "dealerdirect/qa-tools": "All the PHP QA tools you'll need"
- },
- "type": "composer-plugin",
- "extra": {
- "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
- },
- "autoload": {
- "psr-4": {
- "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Franck Nijhof",
- "email": "f.nijhof@dealerdirect.nl",
- "homepage": "http://workingatdealerdirect.eu",
- "role": "Developer"
- }
- ],
- "description": "PHP_CodeSniffer Standards Composer Installer Plugin",
- "homepage": "http://workingatdealerdirect.eu",
- "keywords": [
- "PHPCodeSniffer",
- "PHP_CodeSniffer",
- "code quality",
- "codesniffer",
- "composer",
- "installer",
- "phpcs",
- "plugin",
- "qa",
- "quality",
- "standard",
- "standards",
- "style guide",
- "stylecheck",
- "tests"
- ],
- "time": "2017-09-18T07:49:36+00:00"
- },
- {
- "name": "squizlabs/php_codesniffer",
- "version": "3.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "d667e245d5dcd4d7bf80f26f2c947d476b66213e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d667e245d5dcd4d7bf80f26f2c947d476b66213e",
- "reference": "d667e245d5dcd4d7bf80f26f2c947d476b66213e",
- "shasum": ""
- },
- "require": {
- "ext-simplexml": "*",
- "ext-tokenizer": "*",
- "ext-xmlwriter": "*",
- "php": ">=5.4.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0"
- },
- "bin": [
- "bin/phpcs",
- "bin/phpcbf"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.x-dev"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Greg Sherwood",
- "role": "lead"
- }
- ],
- "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
- "homepage": "http://www.squizlabs.com/php-codesniffer",
- "keywords": [
- "phpcs",
- "standards"
- ],
- "time": "2017-10-16T22:40:25+00:00"
- },
- {
- "name": "wimg/php-compatibility",
- "version": "8.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/wimg/PHPCompatibility.git",
- "reference": "4c4385fb891dff0501009670f988d4fe36785249"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/wimg/PHPCompatibility/zipball/4c4385fb891dff0501009670f988d4fe36785249",
- "reference": "4c4385fb891dff0501009670f988d4fe36785249",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3",
- "squizlabs/php_codesniffer": "^2.2 || ^3.0.2"
- },
- "conflict": {
- "squizlabs/php_codesniffer": "2.6.2"
- },
- "require-dev": {
- "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0"
- },
- "suggest": {
- "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1"
- },
- "type": "phpcodesniffer-standard",
- "autoload": {
- "psr-4": {
- "PHPCompatibility\\": "PHPCompatibility/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0"
- ],
- "authors": [
- {
- "name": "Wim Godden",
- "role": "lead"
- }
- ],
- "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP version compatibility.",
- "homepage": "http://techblog.wimgodden.be/tag/codesniffer/",
- "keywords": [
- "compatibility",
- "phpcs",
- "standards"
- ],
- "time": "2017-08-07T19:39:05+00:00"
- },
- {
- "name": "wp-coding-standards/wpcs",
- "version": "0.14.0",
- "source": {
- "type": "git",
- "url": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git",
- "reference": "8cadf48fa1c70b2381988e0a79e029e011a8f41c"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/8cadf48fa1c70b2381988e0a79e029e011a8f41c",
- "reference": "8cadf48fa1c70b2381988e0a79e029e011a8f41c",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3",
- "squizlabs/php_codesniffer": "^2.9.0 || ^3.0.2"
- },
- "suggest": {
- "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3"
- },
- "type": "phpcodesniffer-standard",
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Contributors",
- "homepage": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/graphs/contributors"
- }
- ],
- "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions",
- "keywords": [
- "phpcs",
- "standards",
- "wordpress"
- ],
- "time": "2017-11-01T15:10:46+00:00"
- }
- ],
- "aliases": [],
- "minimum-stability": "stable",
- "stability-flags": [],
- "prefer-stable": false,
- "prefer-lowest": false,
- "platform": [],
- "platform-dev": []
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "6294e908cbc6a7c0e62e92f44e7a428c",
+ "packages": [
+ {
+ "name": "composer/installers",
+ "version": "v1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/installers.git",
+ "reference": "9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/installers/zipball/9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b",
+ "reference": "9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0"
+ },
+ "replace": {
+ "roundcube/plugin-installer": "*",
+ "shama/baton": "*"
+ },
+ "require-dev": {
+ "composer/composer": "1.0.*@dev",
+ "phpunit/phpunit": "4.1.*"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Composer\\Installers\\Plugin",
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Installers\\": "src/Composer/Installers"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kyle Robinson Young",
+ "email": "kyle@dontkry.com",
+ "homepage": "https://github.com/shama"
+ }
+ ],
+ "description": "A multi-framework Composer library installer",
+ "homepage": "https://composer.github.io/installers/",
+ "keywords": [
+ "Craft",
+ "Dolibarr",
+ "Eliasis",
+ "Hurad",
+ "ImageCMS",
+ "Kanboard",
+ "Lan Management System",
+ "MODX Evo",
+ "Mautic",
+ "Maya",
+ "OXID",
+ "Plentymarkets",
+ "Porto",
+ "RadPHP",
+ "SMF",
+ "Thelia",
+ "WolfCMS",
+ "agl",
+ "aimeos",
+ "annotatecms",
+ "attogram",
+ "bitrix",
+ "cakephp",
+ "chef",
+ "cockpit",
+ "codeigniter",
+ "concrete5",
+ "croogo",
+ "dokuwiki",
+ "drupal",
+ "eZ Platform",
+ "elgg",
+ "expressionengine",
+ "fuelphp",
+ "grav",
+ "installer",
+ "itop",
+ "joomla",
+ "kohana",
+ "laravel",
+ "lavalite",
+ "lithium",
+ "magento",
+ "mako",
+ "mediawiki",
+ "modulework",
+ "moodle",
+ "osclass",
+ "phpbb",
+ "piwik",
+ "ppi",
+ "puppet",
+ "reindex",
+ "roundcube",
+ "shopware",
+ "silverstripe",
+ "sydes",
+ "symfony",
+ "typo3",
+ "wordpress",
+ "yawik",
+ "zend",
+ "zikula"
+ ],
+ "time": "2017-08-09T07:53:48+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "dealerdirect/phpcodesniffer-composer-installer",
+ "version": "v0.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/DealerDirect/phpcodesniffer-composer-installer.git",
+ "reference": "63c0ec0ac286d31651d3c70e5bf76ad87db3ba23"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/DealerDirect/phpcodesniffer-composer-installer/zipball/63c0ec0ac286d31651d3c70e5bf76ad87db3ba23",
+ "reference": "63c0ec0ac286d31651d3c70e5bf76ad87db3ba23",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0",
+ "php": "^5.3|^7",
+ "squizlabs/php_codesniffer": "*"
+ },
+ "require-dev": {
+ "composer/composer": "*",
+ "wimg/php-compatibility": "^8.0"
+ },
+ "suggest": {
+ "dealerdirect/qa-tools": "All the PHP QA tools you'll need"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Franck Nijhof",
+ "email": "f.nijhof@dealerdirect.nl",
+ "homepage": "http://workingatdealerdirect.eu",
+ "role": "Developer"
+ }
+ ],
+ "description": "PHP_CodeSniffer Standards Composer Installer Plugin",
+ "homepage": "http://workingatdealerdirect.eu",
+ "keywords": [
+ "PHPCodeSniffer",
+ "PHP_CodeSniffer",
+ "code quality",
+ "codesniffer",
+ "composer",
+ "installer",
+ "phpcs",
+ "plugin",
+ "qa",
+ "quality",
+ "standard",
+ "standards",
+ "style guide",
+ "stylecheck",
+ "tests"
+ ],
+ "time": "2017-09-18T07:49:36+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
+ "reference": "d667e245d5dcd4d7bf80f26f2c947d476b66213e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d667e245d5dcd4d7bf80f26f2c947d476b66213e",
+ "reference": "d667e245d5dcd4d7bf80f26f2c947d476b66213e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0"
+ },
+ "bin": [
+ "bin/phpcs",
+ "bin/phpcbf"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "lead"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "http://www.squizlabs.com/php-codesniffer",
+ "keywords": [
+ "phpcs",
+ "standards"
+ ],
+ "time": "2017-10-16T22:40:25+00:00"
+ },
+ {
+ "name": "wimg/php-compatibility",
+ "version": "8.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/wimg/PHPCompatibility.git",
+ "reference": "4c4385fb891dff0501009670f988d4fe36785249"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/wimg/PHPCompatibility/zipball/4c4385fb891dff0501009670f988d4fe36785249",
+ "reference": "4c4385fb891dff0501009670f988d4fe36785249",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3",
+ "squizlabs/php_codesniffer": "^2.2 || ^3.0.2"
+ },
+ "conflict": {
+ "squizlabs/php_codesniffer": "2.6.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0"
+ },
+ "suggest": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1"
+ },
+ "type": "phpcodesniffer-standard",
+ "autoload": {
+ "psr-4": {
+ "PHPCompatibility\\": "PHPCompatibility/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0"
+ ],
+ "authors": [
+ {
+ "name": "Wim Godden",
+ "role": "lead"
+ }
+ ],
+ "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP version compatibility.",
+ "homepage": "http://techblog.wimgodden.be/tag/codesniffer/",
+ "keywords": [
+ "compatibility",
+ "phpcs",
+ "standards"
+ ],
+ "time": "2017-08-07T19:39:05+00:00"
+ },
+ {
+ "name": "wp-coding-standards/wpcs",
+ "version": "0.14.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git",
+ "reference": "8cadf48fa1c70b2381988e0a79e029e011a8f41c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/8cadf48fa1c70b2381988e0a79e029e011a8f41c",
+ "reference": "8cadf48fa1c70b2381988e0a79e029e011a8f41c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3",
+ "squizlabs/php_codesniffer": "^2.9.0 || ^3.0.2"
+ },
+ "suggest": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3"
+ },
+ "type": "phpcodesniffer-standard",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "wordpress"
+ ],
+ "time": "2017-11-01T15:10:46+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": []
}
diff --git a/cypress.json b/cypress.json
index f0232a9c12b105..87ff4b29a8f833 100644
--- a/cypress.json
+++ b/cypress.json
@@ -7,5 +7,6 @@
"integrationFolder": "test/e2e/integration",
"supportFile": "test/e2e/support",
"videoRecording": false,
- "chromeWebSecurity": false
+ "chromeWebSecurity": false,
+ "pluginsFile": "test/e2e/plugins"
}
diff --git a/data/README.md b/data/README.md
index a1ca855a38bc65..ce2f42e09274d8 100644
--- a/data/README.md
+++ b/data/README.md
@@ -10,16 +10,55 @@ This module holds a global state variable and exposes a "Redux-like" API contain
If your module or plugin needs to store and manipulate client-side data, you'll have to register a "reducer" to do so. A reducer is a function taking the previous `state` and `action` and returns an update `state`. You can learn more about reducers on the [Redux Documentation](https://redux.js.org/docs/basics/Reducers.html)
-This function takes two arguments: a `key` to identify the module (example: `myAwesomePlugin`) and the reducer function. It returns a Redux-like store object with the following methods:
+This function takes two arguments: a `key` to identify the module (example: `myAwesomePlugin`) and the reducer function. It returns a [Redux-like store object](https://redux.js.org/docs/basics/Store.html) with the following methods:
#### `store.getState()`
-Returns the state object of the registered reducer.
+Returns the [state object](https://redux.js.org/docs/Glossary.html#state) of the registered reducer. See: https://redux.js.org/docs/api/Store.html#getState
#### `store.subscribe( listener: function )`
-Registers a `listener` function called everytime the state is updated.
+Registers a [`listener`](https://redux.js.org/docs/api/Store.html#subscribe) function called everytime the state is updated.
#### `store.dispatch( action: object )`
-The dispatch function should be called to trigger the registered reducers function and update the state. An `action` object should be passed to this action. This action is passed to the registered reducers in addition to the previous state.
\ No newline at end of file
+The dispatch function should be called to trigger the registered reducers function and update the state. An [`action`](https://redux.js.org/docs/api/Store.html#dispatch) object should be passed to this function. This action is passed to the registered reducers in addition to the previous state.
+
+
+### `wp.data.registerSelectors( reducerKey: string, newSelectors: object )`
+
+If your module or plugin needs to expose its state to other modules and plugins, you'll have to register state selectors.
+
+A selector is a function that takes the current state value as a first argument and extra arguments if needed and returns any data extracted from the state.
+
+#### Example:
+
+Let's say the state of our plugin (registered with the key `myPlugin`) has the following shape: `{ title: 'My post title' }`. We can register a `getTitle` selector to make this state value available like so:
+
+```js
+wp.data.registerSelectors( 'myPlugin', { getTitle: ( state ) => state.title } );
+```
+
+### `wp.data.select( key: string, selectorName: string, ...args )`
+
+This function allows calling any registered selector. Given a module's key, a selector's name and extra arguments passed to the selector, this function calls the selector passing it the current state and the extra arguments provided.
+
+#### Example:
+
+```js
+wp.data.select( 'myPlugin', 'getTitle' ); // Returns "My post title"
+```
+
+### `wp.data.query( mapSelectorsToProps: func )( WrappedComponent: Component )`
+
+If you use a React or WordPress Element, a Higher Order Component is made available to inject data into your components like so:
+
+```js
+const Component = ( { title } ) => { title }
;
+
+wp.data.query( select => {
+ return {
+ title: select( 'myPlugin', 'getTitle' ),
+ };
+} )( Component );
+```
diff --git a/data/index.js b/data/index.js
index 8cba209ecbf590..70b20456472267 100644
--- a/data/index.js
+++ b/data/index.js
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+import { connect } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import { flowRight } from 'lodash';
@@ -8,6 +9,7 @@ import { flowRight } from 'lodash';
* Module constants
*/
const reducers = {};
+const selectors = {};
const enhancers = [];
if ( window.__REDUX_DEVTOOLS_EXTENSION__ ) {
enhancers.push( window.__REDUX_DEVTOOLS_EXTENSION__() );
@@ -17,17 +19,17 @@ const initialReducer = () => ( {} );
const store = createStore( initialReducer, {}, flowRight( enhancers ) );
/**
- * Registers a new sub reducer to the global state and returns a Redux-like store object.
+ * Registers a new sub-reducer to the global state and returns a Redux-like store object.
*
- * @param {String} key Reducer key
- * @param {Object} reducer Reducer function
+ * @param {string} reducerKey Reducer key.
+ * @param {Object} reducer Reducer function.
*
- * @return {Object} Store Object
+ * @returns {Object} Store Object.
*/
-export function registerReducer( key, reducer ) {
- reducers[ key ] = reducer;
+export function registerReducer( reducerKey, reducer ) {
+ reducers[ reducerKey ] = reducer;
store.replaceReducer( combineReducers( reducers ) );
- const getState = () => store.getState()[ key ];
+ const getState = () => store.getState()[ reducerKey ];
return {
dispatch: store.dispatch,
@@ -46,3 +48,56 @@ export function registerReducer( key, reducer ) {
getState,
};
}
+
+/**
+ * Registers selectors for external usage.
+ *
+ * @param {string} reducerKey Part of the state shape to register the
+ * selectors for.
+ * @param {Object} newSelectors Selectors to register. Keys will be used as the
+ * public facing API. Selectors will get passed the
+ * state as first argument.
+ */
+export function registerSelectors( reducerKey, newSelectors ) {
+ selectors[ reducerKey ] = newSelectors;
+}
+
+/**
+ * Higher Order Component used to inject data using the registered selectors.
+ *
+ * @param {Function} mapSelectorsToProps Gets called with the selectors object
+ * to determine the data for the
+ * component.
+ *
+ * @returns {Function} Renders the wrapped component and passes it data.
+ */
+export const query = ( mapSelectorsToProps ) => ( WrappedComponent ) => {
+ const connectWithStore = ( ...args ) => {
+ const ConnectedWrappedComponent = connect( ...args )( WrappedComponent );
+ return ( props ) => {
+ return ;
+ };
+ };
+
+ return connectWithStore( ( state, ownProps ) => {
+ const select = ( key, selectorName, ...args ) => {
+ return selectors[ key ][ selectorName ]( state[ key ], ...args );
+ };
+
+ return mapSelectorsToProps( select, ownProps );
+ } );
+};
+
+/**
+ * Calls a selector given the current state and extra arguments.
+ *
+ * @param {string} reducerKey Part of the state shape to register the
+ * selectors for.
+ * @param {string} selectorName Selector name.
+ * @param {*} args Selectors arguments.
+ *
+ * @returns {*} The selector's returned value.
+ */
+export const select = ( reducerKey, selectorName, ...args ) => {
+ return selectors[ reducerKey ][ selectorName ]( store.getState()[ reducerKey ], ...args );
+};
diff --git a/data/test/__snapshots__/index.js.snap b/data/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..46758a91bd19a8
--- /dev/null
+++ b/data/test/__snapshots__/index.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`query passes the relevant data to the component 1`] = `
+
+ reactState
+
+`;
diff --git a/data/test/index.js b/data/test/index.js
index 4e0a2d68d4475c..08d56fed3354d5 100644
--- a/data/test/index.js
+++ b/data/test/index.js
@@ -1,4 +1,12 @@
-import { registerReducer } from '../';
+/**
+ * External dependencies
+ */
+import { render } from 'enzyme';
+
+/**
+ * Internal dependencies
+ */
+import { registerReducer, registerSelectors, select, query } from '../';
describe( 'store', () => {
it( 'Should append reducers to the state', () => {
@@ -12,3 +20,42 @@ describe( 'store', () => {
expect( store2.getState() ).toEqual( 'ribs' );
} );
} );
+
+describe( 'select', () => {
+ it( 'registers multiple selectors to the public API', () => {
+ const store = registerReducer( 'reducer1', () => 'state1' );
+ const selector1 = jest.fn( () => 'result1' );
+ const selector2 = jest.fn( () => 'result2' );
+
+ registerSelectors( 'reducer1', {
+ selector1,
+ selector2,
+ } );
+
+ expect( select( 'reducer1', 'selector1' ) ).toEqual( 'result1' );
+ expect( selector1 ).toBeCalledWith( store.getState() );
+
+ expect( select( 'reducer1', 'selector2' ) ).toEqual( 'result2' );
+ expect( selector2 ).toBeCalledWith( store.getState() );
+ } );
+} );
+
+describe( 'query', () => {
+ it( 'passes the relevant data to the component', () => {
+ registerReducer( 'reactReducer', () => ( { reactKey: 'reactState' } ) );
+ registerSelectors( 'reactReducer', {
+ reactSelector: ( state, key ) => state[ key ],
+ } );
+ const Component = query( ( selectFunc, ownProps ) => {
+ return {
+ data: selectFunc( 'reactReducer', 'reactSelector', ownProps.keyName ),
+ };
+ } )( ( props ) => {
+ return { props.data }
;
+ } );
+
+ const tree = render( );
+
+ expect( tree ).toMatchSnapshot();
+ } );
+} );
diff --git a/date/index.js b/date/index.js
index 5b1dfb7f1fc8cb..f8b7a83ee712fd 100644
--- a/date/index.js
+++ b/date/index.js
@@ -44,7 +44,8 @@ const formatMap = {
* Gets the ordinal suffix.
*
* @param {moment} momentDate Moment instance.
- * @return {string} Formatted date.
+ *
+ * @returns {string} Formatted date.
*/
S( momentDate ) {
// Do - D
@@ -58,7 +59,8 @@ const formatMap = {
* Gets the day of the year (zero-indexed).
*
* @param {moment} momentDate Moment instance.
- * @return {string} Formatted date.
+ *
+ * @returns {string} Formatted date.
*/
z( momentDate ) {
// DDD - 1
@@ -77,7 +79,8 @@ const formatMap = {
* Gets the days in the month.
*
* @param {moment} momentDate Moment instance.
- * @return {string} Formatted date.
+ *
+ * @returns {string} Formatted date.
*/
t( momentDate ) {
return momentDate.daysInMonth();
@@ -88,7 +91,8 @@ const formatMap = {
* Gets whether the current year is a leap year.
*
* @param {moment} momentDate Moment instance.
- * @return {string} Formatted date.
+ *
+ * @returns {string} Formatted date.
*/
L( momentDate ) {
return momentDate.isLeapYear() ? '1' : '0';
@@ -104,7 +108,8 @@ const formatMap = {
* Gets the current time in Swatch Internet Time (.beats).
*
* @param {moment} momentDate Moment instance.
- * @return {string} Formatted date.
+ *
+ * @returns {string} Formatted date.
*/
B( momentDate ) {
const timezoned = moment( momentDate ).utcOffset( 60 );
@@ -134,7 +139,8 @@ const formatMap = {
* Gets whether the timezone is in DST currently.
*
* @param {moment} momentDate Moment instance.
- * @return {string} Formatted date.
+ *
+ * @returns {string} Formatted date.
*/
I( momentDate ) {
return momentDate.isDST() ? '1' : '0';
@@ -146,7 +152,8 @@ const formatMap = {
* Gets the timezone offset in seconds.
*
* @param {moment} momentDate Moment instance.
- * @return {string} Formatted date.
+ *
+ * @returns {string} Formatted date.
*/
Z( momentDate ) {
// Timezone offset in seconds.
@@ -214,11 +221,12 @@ function setupLocale( settings ) {
/**
* Formats a date. Does not alter the date's timezone.
*
- * @param {string} dateFormat PHP-style formatting string.
- * See php.net/date
- * @param {(Date|string|moment|null)} dateValue Date object or string,
- * parsable by moment.js.
- * @return {string} Formatted date.
+ * @param {string} dateFormat PHP-style formatting string.
+ * See php.net/date.
+ * @param {(Date|string|moment|null)} dateValue Date object or string,
+ * parsable by moment.js.
+ *
+ * @returns {string} Formatted date.
*/
export function format( dateFormat, dateValue = new Date() ) {
let i, char;
@@ -254,12 +262,12 @@ export function format( dateFormat, dateValue = new Date() ) {
/**
* Formats a date (like `date()` in PHP), in the site's timezone.
*
- * @param {string} dateFormat PHP-style formatting string.
- * See php.net/date
- * @param {(Date|string|moment|null)} dateValue Date object or string,
- * parsable by moment.js.
+ * @param {string} dateFormat PHP-style formatting string.
+ * See php.net/date.
+ * @param {(Date|string|moment|null)} dateValue Date object or string,
+ * parsable by moment.js.
*
- * @return {string} Formatted date.
+ * @returns {string} Formatted date.
*/
export function date( dateFormat, dateValue = new Date() ) {
const offset = window._wpDateSettings.timezone.offset * HOUR_IN_MINUTES;
@@ -270,12 +278,12 @@ export function date( dateFormat, dateValue = new Date() ) {
/**
* Formats a date (like `date()` in PHP), in the UTC timezone.
*
- * @param {string} dateFormat PHP-style formatting string.
- * See php.net/date
- * @param {(Date|string|moment|null)} dateValue Date object or string,
- * parsable by moment.js.
+ * @param {string} dateFormat PHP-style formatting string.
+ * See php.net/date.
+ * @param {(Date|string|moment|null)} dateValue Date object or string,
+ * parsable by moment.js.
*
- * @return {string} Formatted date.
+ * @returns {string} Formatted date.
*/
export function gmdate( dateFormat, dateValue = new Date() ) {
const dateMoment = moment( dateValue ).utc();
@@ -285,14 +293,14 @@ export function gmdate( dateFormat, dateValue = new Date() ) {
/**
* Formats a date (like `dateI18n()` in PHP).
*
- * @param {string} dateFormat PHP-style formatting string.
- * See php.net/date
- * @param {(Date|string|moment|null)} dateValue Date object or string,
- * parsable by moment.js.
- * @param {boolean} gmt True for GMT/UTC, false for
- * site's timezone.
+ * @param {string} dateFormat PHP-style formatting string.
+ * See php.net/date.
+ * @param {(Date|string|moment|null)} dateValue Date object or string,
+ * parsable by moment.js.
+ * @param {boolean} gmt True for GMT/UTC, false for
+ * site's timezone.
*
- * @return {string} Formatted date.
+ * @returns {string} Formatted date.
*/
export function dateI18n( dateFormat, dateValue = new Date(), gmt = false ) {
// Defaults.
diff --git a/docker/docker-compose.yml b/docker-compose.yml
similarity index 59%
rename from docker/docker-compose.yml
rename to docker-compose.yml
index 365dbc0959c707..22ff627e88bf23 100644
--- a/docker/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,8 +10,14 @@ services:
WORDPRESS_DB_PASSWORD: example
ABSPATH: /usr/src/wordpress/
volumes:
- - ../:/var/www/html/wp-content/plugins/gutenberg
- container_name: wordpress-dev
+ - wordpress:/var/www/html
+ - .:/var/www/html/wp-content/plugins/gutenberg
+
+ cli:
+ image: wordpress:cli
+ volumes:
+ - wordpress:/var/www/html
+ - .:/var/www/html/wp-content/plugins/gutenberg
mysql:
image: mysql:5.7
@@ -24,8 +30,14 @@ services:
environment:
PHPUNIT_DB_HOST: mysql
volumes:
- - ..:/app
+ - .:/app
- testsuite:/tmp
+ composer:
+ image: composer
+ volumes:
+ - .:/app
+
volumes:
- testsuite: {}
+ testsuite:
+ wordpress:
diff --git a/docs/block-api.md b/docs/block-api.md
index e29190f3d2c6fc..b4d55b245cc7bc 100644
--- a/docs/block-api.md
+++ b/docs/block-api.md
@@ -116,7 +116,7 @@ Work in progress...
* **Type:** `Bool`
* **Default:** `false`
-Whether a block can only be used once per post.
+A once-only block can be inserted into each post, one time only. For example, the built-in 'More' block cannot be inserted again if it already exists in the post being edited. A once-only block's icon is automatically dimmed (unclickable) to prevent multiple instances.
```js
// Use the block just once per post
diff --git a/docs/blocks-scaffolding.md b/docs/blocks-scaffolding.md
new file mode 100644
index 00000000000000..6c8b19a0c56c95
--- /dev/null
+++ b/docs/blocks-scaffolding.md
@@ -0,0 +1,239 @@
+# Generate Blocks with WP-CLI
+
+It turns out that writing the simplest possible block which contains only static content might not be the easiest task. It requires to follow closely the steps described in the documentation. It stems from the fact that you need to create at least 2 files and integrate your code with the existing APIs. One way to mitigate this inconvenience is to copy the source code of the existing block from one of the repositories that share working examples:
+- [WordPress/gutenberg-examples](https://github.com/WordPress/gutenberg-examples) - the official examples for extending Gutenberg with plugins which create blocks
+- [zgordon/gutenberg-course](https://github.com/zgordon/gutenberg-course) - a repository for Zac Gordon's Gutenberg Development Course
+- [ahmadawais/Gutenberg-Boilerplate](https://github.com/ahmadawais/Gutenberg-Boilerplate) - an inline documented starter WordPress plugin for the new Gutenberg editor
+
+## WP-CLI
+
+Another way of making developer's life easier is to use [WP-CLI](http://wp-cli.org/), which provides a command-line interface for many actions you might perform on the WordPress instance. One of the commands generates all the code required to register a Gutenberg block for a plugin or theme.
+
+### Installing
+
+Before installing `WP-CLI`, please make sure your environment meets the minimum requirements:
+
+* UNIX-like environment (OS X, Linux, FreeBSD, Cygwin); limited support in Windows environment
+* PHP 5.3.29 or later
+* WordPress 3.7 or later
+
+Once you’ve verified requirements, you should follow the [installation instructions](http://wp-cli.org/#installing). Downloading the Phar file is the recommended installation method for most users. Should you need, see also the documentation on [alternative installation methods](https://make.wordpress.org/cli/handbook/installing/).
+
+_Important_: To use scaffolding command for blocks you temporary need (until v1.5.0 is released) to run `wp cli update --nightly` to use the latest nightly build of `WP-CLI`. The nightly build is more or less stable enough for you to use in your development environment, and always includes the latest and greatest `WP-CLI` features.
+
+### Using `wp scaffold block`
+
+The following command generates PHP, JS and CSS code for registering a Gutenberg block.
+
+```bash
+wp scaffold block [--title=] [--dashicon=] [--category=] [--theme] [--plugin=] [--force]
+```
+
+Please refer to the [command documentation](https://github.com/wp-cli/scaffold-command#wp-scaffold-block) to learn more about the available options for the block.
+
+When you scaffold a block you must provide at least a `slug` name and either the `theme` or `plugin` name. In most cases, we recommended pairing blocks with plugins rather than themes, because only using plugin ensures that all blocks will still work when your theme changes.
+
+### Examples
+
+Here are some examples using `wp scaffold block` in action.
+
+#### Create a block inside the plugin
+
+The following command generates a `movie` block with the `My movie block` title for the existing plugin named `movies`:
+
+```bash
+$ wp scaffold block movie --title="My movie block" --plugin=movies
+Success: Created block 'My movie block'.
+```
+
+This will generate 4 files inside the `movies` plugin directory. All files contain inline documentation that will help to apply any further customizations to the block. It's worth mentioning that it is currently possible to scaffold only blocks containing static content and JavaScript code is written using ECMAScript 5 (ES5) standard which works with all modern browsers.
+
+`movies/blocks/movie.php` - you will have to manually include this file in your main plugin file:
+```php
+ 'movie-block-editor',
+ 'editor_style' => 'movie-block-editor',
+ 'style' => 'movie-block',
+ ) );
+}
+add_action( 'init', 'movie_block_init' );
+```
+
+`movies/blocks/movie/block.js`:
+```js
+( function( wp ) {
+ /**
+ * Registers a new block provided a unique name and an object defining its behavior.
+ * @see https://github.com/WordPress/gutenberg/tree/master/blocks#api
+ */
+ var registerBlockType = wp.blocks.registerBlockType;
+ /**
+ * Returns a new element of given type. Element is an abstraction layer atop React.
+ * @see https://github.com/WordPress/gutenberg/tree/master/element#element
+ */
+ var el = wp.element.createElement;
+ /**
+ * Retrieves the translation of text.
+ * @see https://github.com/WordPress/gutenberg/tree/master/i18n#api
+ */
+ var __ = wp.i18n.__;
+
+ /**
+ * Every block starts by registering a new block type definition.
+ * @see https://wordpress.org/gutenberg/handbook/block-api/
+ */
+ registerBlockType( 'movies/movie', {
+ /**
+ * This is the display title for your block, which can be translated with `i18n` functions.
+ * The block inserter will show this name.
+ */
+ title: __( 'My movie block' ),
+
+ /**
+ * Blocks are grouped into categories to help users browse and discover them.
+ * The categories provided by core are `common`, `embed`, `formatting`, `layout` and `widgets`.
+ */
+ category: 'widgets',
+
+ /**
+ * Optional block extended support features.
+ */
+ supports: {
+ // Removes support for an HTML mode.
+ html: false,
+ },
+
+ /**
+ * The edit function describes the structure of your block in the context of the editor.
+ * This represents what the editor will render when the block is used.
+ * @see https://wordpress.org/gutenberg/handbook/block-edit-save/#edit
+ *
+ * @param {Object} [props] Properties passed from the editor.
+ * @return {Element} Element to render.
+ */
+ edit: function( props ) {
+ return el(
+ 'p',
+ { className: props.className },
+ __( 'Hello from the editor!' )
+ );
+ },
+
+ /**
+ * The save function defines the way in which the different attributes should be combined
+ * into the final markup, which is then serialized by Gutenberg into `post_content`.
+ * @see https://wordpress.org/gutenberg/handbook/block-edit-save/#save
+ *
+ * @return {Element} Element to render.
+ */
+ save: function() {
+ return el(
+ 'p',
+ {},
+ __( 'Hello from the saved content!' )
+ );
+ }
+ } );
+} )(
+ window.wp
+);
+```
+
+`movies/blocks/movie/editor.css`:
+```css
+/**
+ * The following styles get applied inside the editor only.
+ *
+ * Replace them with your own styles or remove the file completely.
+ */
+
+.wp-block-movies-movie {
+ border: 1px dotted #f00;
+}
+```
+
+`movies/blocks/movie/style.css`:
+```css
+/**
+ * The following styles get applied both on the front of your site and in the editor.
+ *
+ * Replace them with your own styles or remove the file completely.
+ */
+
+.wp-block-movies-movie {
+ background-color: #000;
+ color: #fff;
+ padding: 2px;
+}
+```
+
+#### Create a block inside the theme
+
+It is also possible to scaffold the same `movie` block and include it into the existing theme, e.g. `simple-life`:
+
+```bash
+$ wp scaffold block movie --theme=simple-life
+ Success: Created block 'Movie block'.
+```
+
+#### Create a plugin and add two blocks
+
+If you don't have an existing plugin you can create a new one and add two blocks with `WP-CLI` as follows:
+
+```bash
+# Create plugin called books
+$ wp scaffold plugin books
+# Add a block called book to plugin books
+$ wp scaffold block book --title="Book" --plugin=books
+# Add a second block to plugin called books.
+$ wp scaffold block books --title="Book List" --plugin=books
+```
diff --git a/docs/coding-guidelines.md b/docs/coding-guidelines.md
index e73d565c56e6a0..ec45372e0c3474 100644
--- a/docs/coding-guidelines.md
+++ b/docs/coding-guidelines.md
@@ -91,6 +91,6 @@ import VisualEditor from '../visual-editor';
We use
[`phpcs` (PHP\_CodeSniffer)](https://github.com/squizlabs/PHP_CodeSniffer) with the [WordPress Coding Standards ruleset](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards) to run a lot of automated checks against all PHP code in this project. This ensures that we are consistent with WordPress PHP coding standards.
-When making any changes to the PHP code in this project, it's recommended to install and run `phpcs` on your computer. This is a step in our Travis CI build as well, but it is better to catch errors locally.
+The easiest way to use PHPCS is [local environment](https://github.com/WordPress/gutenberg/blob/master/CONTRIBUTING.md#local-environment). Once that's installed, you can check your PHP by running `npm run lint-php`.
-The easiest way to do this is using `composer`. [Install `composer`](https://getcomposer.org/download/) on your computer, then run `composer install`. This will install `phpcs` and `WordPress-Coding-Standards` which you can the run via `vendor/bin/phpcs`.
+If you prefer to install PHPCS locally, you should use `composer`. [Install `composer`](https://getcomposer.org/download/) on your computer, then run `composer install`. This will install `phpcs` and `WordPress-Coding-Standards` which you can the run via `vendor/bin/phpcs`.
diff --git a/docs/manifest.json b/docs/manifest.json
index f6ef83fd9094a9..2604d268a083d8 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -89,6 +89,12 @@
"markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/blocks-dynamic.md",
"parent": "blocks"
},
+ {
+ "title": "Generate Blocks with WP-CLI",
+ "slug": "generate-blocks-with-wp-cli",
+ "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/blocks-scaffolding.md",
+ "parent": "blocks"
+ },
{
"title": "Reference",
"slug": "reference",
diff --git a/docs/meta-box.md b/docs/meta-box.md
index d3fe4d2da2e995..cb66c5405abfba 100644
--- a/docs/meta-box.md
+++ b/docs/meta-box.md
@@ -57,11 +57,9 @@ When rendering the Gutenberg Page, the metaboxes are rendered to a hidden div `#
#### MetaBoxArea Component
-When the component renders it will store a ref to the metaboxes container, retrieve the metaboxes HTML from the prefetch location and watches input and changes.
+When the component renders it will store a ref to the metaboxes container, retrieve the metaboxes HTML from the prefetch location.
-The change detection will store the current form's `FormData`, then whenever a change is detected the current form data will be checked vs, the original form data. This serves as a way to see if the meta box state is dirty. When the meta box state has been detected to have changed, a Redux action `META_BOX_STATE_CHANGED` is dispatched, updating the store setting the isDirty flag to `true`. If the state ever returns back to the original form data, `META_BOX_STATE_CHANGED` is dispatched again to set the isDirty flag to `false`. A selector `isMetaBoxStateDirty()` is used to help check whether the post can be updated. It checks each meta box for whether it is dirty, and if there is at least one dirty meta box, it will return true. This dirty detection does not impact creating new posts, as the content will have to change before meta boxes can trigger the overall dirty state.
-
-When the post is updated, only meta boxes areas that are active and dirty, will be submitted. This removes any unnecessary requests being made. No extra revisions, are created either by the meta box submissions. A Redux action will trigger on `REQUEST_POST_UPDATE` for any dirty meta box. See `editor/effects.js`. The `REQUEST_META_BOX_UPDATES` action will set that meta boxes' state to `isUpdating`, the `isUpdating` prop will be sent into the `MetaBoxArea` and cause a form submission.
+When the post is updated, only meta boxes areas that are active will be submitted. This removes any unnecessary requests being made. No extra revisions, are created either by the meta box submissions. A Redux action will trigger on `REQUEST_POST_UPDATE` for any active meta box. See `editor/effects.js`. The `REQUEST_META_BOX_UPDATES` action will set that meta boxes' state to `isUpdating`, the `isUpdating` prop will be sent into the `MetaBoxArea` and cause a form submission.
If the metabox area is saving, we display an updating overlay, to prevent users from changing the form values while the meta box is submitting.
@@ -75,7 +73,7 @@ So an example url would look like:
This url is automatically passed into React via a `_wpMetaBoxUrl` global variable.
-Thus page page mimics the `post.php` post form, so when it is submitted it will normally fire all of the necessary hooks and actions, and have the proper global state to correctly fire any PHP meta box mumbo jumbo without needing to modify any existing code. On successful submission, React will signal a `handleMetaBoxReload` to set up the new form state for dirty checking, remove the updating overlay, and set the store to no longer be updating the meta box area.
+Thus page page mimics the `post.php` post form, so when it is submitted it will normally fire all of the necessary hooks and actions, and have the proper global state to correctly fire any PHP meta box mumbo jumbo without needing to modify any existing code. On successful submission, React will signal a `handleMetaBoxReload` to remove the updating overlay, and set the store to no longer be updating the meta box area.
### Common Compatibility Issues
diff --git a/docs/templates.md b/docs/templates.md
index d21d5d653c21e6..c60c56f095bfd0 100644
--- a/docs/templates.md
+++ b/docs/templates.md
@@ -68,3 +68,20 @@ Sometimes the intention might be to lock the template on the UI so that the bloc
- `all` — prevents all operations.
- `insert` — prevents inserting new blocks, but allows moving existing ones.
+
+## Existing Post Types
+
+It is also possible to assign a template to an existing post type like "posts" and "pages":
+
+```php
+function my_add_template_to_posts() {
+ $post_type_object = get_post_type_object( 'post' );
+ $post_type_object->template = array(
+ array( 'core/paragraph', array(
+ 'placeholder' => 'Add Description...',
+ ) ),
+ );
+ $post_type_object->template_lock = 'all';
+}
+add_action( 'init', 'my_add_template_to_posts' );
+```
\ No newline at end of file
diff --git a/editor/assets/stylesheets/_admin-schemes.scss b/editor/assets/stylesheets/_admin-schemes.scss
index bf181b1b9b6b9e..39e6df4550d4bb 100644
--- a/editor/assets/stylesheets/_admin-schemes.scss
+++ b/editor/assets/stylesheets/_admin-schemes.scss
@@ -51,6 +51,19 @@ $scheme-sunrise__spot-color: #de823f;
border-color: $spot-color;
}
}
+
+ // Saving state indicators
+ .editor-post-publish-button.is-saving,
+ .editor-post-publish-button.is-saving:disabled {
+ // Yes, these need to be !important because they !important upstream too ¯\_(ツ)_/¯
+ border-color: darken( $spot-color, 10 ) darken( $spot-color, 20 ) darken( $spot-color, 20 ) !important;
+ box-shadow: 0 1px 0 darken( $spot-color, 20 ) !important;
+ text-shadow: 0 -1px 1px darken( $spot-color, 20 ), 1px 0 1px darken( $spot-color, 20 ), 0 1px 1px darken( $spot-color, 20 ), -1px 0 1px darken( $spot-color, 20 ) !important;
+
+ &:before {
+ background-image: repeating-linear-gradient( -45deg, darken( $spot-color, 20 ), darken( $spot-color, 20 ) 11px, darken( $spot-color, 10 ) 10px, darken( $spot-color, 10 ) 20px );
+ }
+ }
}
}
diff --git a/editor/assets/stylesheets/_mixins.scss b/editor/assets/stylesheets/_mixins.scss
index 28c723bdc3140a..9afaf52f4c0cdd 100644
--- a/editor/assets/stylesheets/_mixins.scss
+++ b/editor/assets/stylesheets/_mixins.scss
@@ -121,27 +121,32 @@ $float-margin: calc( 50% - #{ $visual-editor-max-width-padding / 2 } );
}
@mixin button-style__hover {
- color: $dark-gray-900; // previously $blue-medium-500
+ color: $dark-gray-900;
+ box-shadow: inset 0 0 0 1px $light-gray-500, inset 0 0 0 2px $white, 0 1px 1px rgba( $dark-gray-900, .2 );
+}
+
+@mixin button-style__active() {
+ outline: none;
+ color: $dark-gray-900;
+ box-shadow: inset 0 0 0 1px $light-gray-700, inset 0 0 0 2px $white;
}
@mixin button-style__focus-active() {
outline: none;
- box-shadow: inset 0 0 0 1px $dark-gray-500, inset 0 0 0 2px $white;
color: $dark-gray-900;
- background: $light-gray-300;
+ box-shadow: inset 0 0 0 1px $dark-gray-300, inset 0 0 0 2px $white;
}
@mixin tab-style__focus-active() {
outline: none;
- box-shadow: 0 0 0 1px $dark-gray-500;
color: $dark-gray-900;
- background: $light-gray-300;
+ box-shadow: inset 0 0 0 1px $dark-gray-300;
}
@mixin input-style__focus-active() {
outline: none;
- box-shadow: 0 0 0 1px $dark-gray-500;
color: $dark-gray-900;
+ box-shadow: 0 0 0 1px $dark-gray-300;
}
/**
diff --git a/editor/assets/stylesheets/_z-index.scss b/editor/assets/stylesheets/_z-index.scss
index f218da7a81da5f..6ddcf087abb80a 100644
--- a/editor/assets/stylesheets/_z-index.scss
+++ b/editor/assets/stylesheets/_z-index.scss
@@ -22,7 +22,7 @@ $z-layers: (
'.editor-block-switcher__menu': 2,
'.components-popover__close': 2,
'.editor-block-mover': 1,
- '.blocks-gallery-image__inline-menu': 20,
+ '.blocks-gallery-item__inline-menu': 20,
'.editor-block-settings-menu__popover': 20, // Below the header
'.editor-header': 30,
'.editor-text-editor__formatting': 30,
diff --git a/editor/assets/stylesheets/main.scss b/editor/assets/stylesheets/main.scss
index b01d63b1bb69b8..1a384d5ee041e9 100644
--- a/editor/assets/stylesheets/main.scss
+++ b/editor/assets/stylesheets/main.scss
@@ -30,6 +30,7 @@ body.gutenberg-editor-page {
svg {
fill: currentColor;
+ outline: none;
}
ul#adminmenu a.wp-has-current-submenu:after,
@@ -43,11 +44,11 @@ body.gutenberg-editor-page {
box-sizing: border-box;
}
- ul {
+ ul:not(.wp-block-gallery) {
list-style-type: disc;
}
- ol {
+ ol:not(.wp-block-gallery) {
list-style-type: decimal;
}
diff --git a/editor/components/block-drop-zone/index.js b/editor/components/block-drop-zone/index.js
index 00e43e85372c19..29f76029940b14 100644
--- a/editor/components/block-drop-zone/index.js
+++ b/editor/components/block-drop-zone/index.js
@@ -8,20 +8,26 @@ import { reduce, get, find } from 'lodash';
* WordPress dependencies
*/
import { DropZone, withContext } from '@wordpress/components';
-import { getBlockTypes } from '@wordpress/blocks';
+import { getBlockTypes, rawHandler } from '@wordpress/blocks';
import { compose } from '@wordpress/element';
/**
* Internal dependencies
*/
-import { insertBlocks } from '../../store/actions';
+import { insertBlocks, updateBlockAttributes } from '../../store/actions';
function BlockDropZone( { index, isLocked, ...props } ) {
if ( isLocked ) {
return null;
}
- const dropFiles = ( files, position ) => {
+ const getInsertPosition = ( position ) => {
+ if ( index !== undefined ) {
+ return position.y === 'top' ? index : index + 1;
+ }
+ };
+
+ const onDropFiles = ( files, position ) => {
const transformation = reduce( getBlockTypes(), ( ret, blockType ) => {
if ( ret ) {
return ret;
@@ -33,19 +39,24 @@ function BlockDropZone( { index, isLocked, ...props } ) {
}, false );
if ( transformation ) {
- let insertPosition;
- if ( index !== undefined ) {
- insertPosition = position.y === 'top' ? index : index + 1;
- }
- transformation.transform( files ).then( ( blocks ) => {
- props.insertBlocks( blocks, insertPosition );
- } );
+ const insertPosition = getInsertPosition( position );
+ const blocks = transformation.transform( files, props.updateBlockAttributes );
+ props.insertBlocks( blocks, insertPosition );
+ }
+ };
+
+ const onHTMLDrop = ( HTML, position ) => {
+ const blocks = rawHandler( { HTML, mode: 'BLOCKS' } );
+
+ if ( blocks.length ) {
+ props.insertBlocks( blocks, getInsertPosition( position ) );
}
};
return (
);
}
@@ -53,7 +64,7 @@ function BlockDropZone( { index, isLocked, ...props } ) {
export default compose(
connect(
undefined,
- { insertBlocks }
+ { insertBlocks, updateBlockAttributes }
),
withContext( 'editor' )( ( settings ) => {
const { templateLock } = settings;
diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js
index 529e9e082f6504..f105d199c458e7 100644
--- a/editor/components/block-list/block.js
+++ b/editor/components/block-list/block.js
@@ -70,8 +70,9 @@ const { BACKSPACE, ESCAPE, DELETE, ENTER, UP, RIGHT, DOWN, LEFT } = keycodes;
/**
* Given a DOM node, finds the closest scrollable container node.
*
- * @param {Element} node Node from which to start
- * @return {?Element} Scrollable container node, if found
+ * @param {Element} node Node from which to start.
+ *
+ * @returns {?Element} Scrollable container node, if found.
*/
function getScrollContainer( node ) {
if ( ! node ) {
@@ -103,6 +104,7 @@ export class BlockListBlock extends Component {
this.stopTypingOnMouseMove = this.stopTypingOnMouseMove.bind( this );
this.mergeBlocks = this.mergeBlocks.bind( this );
this.onFocus = this.onFocus.bind( this );
+ this.preventDrag = this.preventDrag.bind( this );
this.onPointerDown = this.onPointerDown.bind( this );
this.onKeyDown = this.onKeyDown.bind( this );
this.onBlockError = this.onBlockError.bind( this );
@@ -223,12 +225,9 @@ export class BlockListBlock extends Component {
}
maybeStartTyping() {
- // We do not want to dispatch start typing if...
- // - State value already reflects that we're typing (dispatch noise)
- // - The current block is not selected (e.g. after a split occurs,
- // we'll still receive the keyDown event, but the focus has since
- // shifted to the newly created block)
- if ( ! this.props.isTyping && this.props.isSelected ) {
+ // We do not want to dispatch start typing if state value already reflects
+ // that we're typing (dispatch noise)
+ if ( ! this.props.isTyping ) {
this.props.onStartTyping();
}
}
@@ -266,18 +265,50 @@ export class BlockListBlock extends Component {
} else {
onMerge( previousBlock, block );
}
+
+ // Manually trigger typing mode, since merging will remove this block and
+ // cause onKeyDown to not fire
+ this.maybeStartTyping();
}
insertBlocksAfter( blocks ) {
this.props.onInsertBlocks( blocks, this.props.order + 1 );
}
+ /**
+ * Marks the block as selected when focused and not already selected. This
+ * specifically handles the case where block does not set focus on its own
+ * (via `setFocus`), typically if there is no focusable input in the block.
+ *
+ * @param {FocusEvent} event A focus event
+ *
+ * @returns {void}
+ */
onFocus( event ) {
- if ( event.target === this.node ) {
+ if ( event.target === this.node && ! this.props.isSelected ) {
this.props.onSelect();
}
}
+ /**
+ * Prevents default dragging behavior within a block to allow for multi-
+ * selection to take effect unhampered.
+ *
+ * @param {DragEvent} event Drag event.
+ *
+ * @returns {void}
+ */
+ preventDrag( event ) {
+ event.preventDefault();
+ }
+
+ /**
+ * Begins tracking cursor multi-selection when clicking down within block.
+ *
+ * @param {MouseEvent} event A mousedown event.
+ *
+ * @returns {void}
+ */
onPointerDown( event ) {
// Not the main button.
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
@@ -292,7 +323,10 @@ export class BlockListBlock extends Component {
}
} else {
this.props.onSelectionStart( this.props.uid );
- this.props.onSelect();
+
+ if ( ! this.props.isSelected ) {
+ this.props.onSelect();
+ }
}
}
@@ -310,6 +344,9 @@ export class BlockListBlock extends Component {
createBlock( 'core/paragraph' ),
], this.props.order + 1 );
}
+
+ // Pressing enter should trigger typing mode after the content has split
+ this.maybeStartTyping();
break;
case UP:
@@ -335,6 +372,9 @@ export class BlockListBlock extends Component {
}
}
}
+
+ // Pressing backspace should trigger typing mode
+ this.maybeStartTyping();
break;
case ESCAPE:
@@ -399,7 +439,7 @@ export class BlockListBlock extends Component {
event.preventDefault() }
+ onDragStart={ this.preventDrag }
onMouseDown={ this.onPointerDown }
onKeyDown={ this.onKeyDown }
onFocus={ this.onFocus }
diff --git a/editor/components/block-list/index.js b/editor/components/block-list/index.js
index 19f7b99bffe713..c6546aeb0b8ec1 100644
--- a/editor/components/block-list/index.js
+++ b/editor/components/block-list/index.js
@@ -34,9 +34,10 @@ import {
getMultiSelectedBlockUids,
getSelectedBlock,
isSelectionEnabled,
+ isMultiSelecting,
} from '../../store/selectors';
import { startMultiSelect, stopMultiSelect, multiSelect, selectBlock } from '../../store/actions';
-import { isInputField } from '../../utils/dom';
+import { documentHasSelection } from '../../utils/dom';
class BlockList extends Component {
constructor( props ) {
@@ -100,7 +101,21 @@ class BlockList extends Component {
}
}
+ /**
+ * Handles a pointer move event to update the extent of the current cursor
+ * multi-selection.
+ *
+ * @param {MouseEvent} event A mousemove event object.
+ *
+ * @returns {void}
+ */
onPointerMove( { clientY } ) {
+ // We don't start multi-selection until the mouse starts moving, so as
+ // to avoid dispatching multi-selection actions on an in-place click.
+ if ( ! this.props.isMultiSelecting ) {
+ this.props.onStartMultiSelect();
+ }
+
const boundaries = this.nodes[ this.selectionAtStart ].getBoundingClientRect();
const y = clientY - boundaries.top;
const key = findLast( this.coordMapKeys, ( coordY ) => coordY < y );
@@ -116,7 +131,7 @@ class BlockList extends Component {
}
// Let native copy behaviour take over in input fields.
- if ( selectedBlock && isInputField( document.activeElement ) ) {
+ if ( selectedBlock && documentHasSelection() ) {
return;
}
@@ -138,6 +153,14 @@ class BlockList extends Component {
}
}
+ /**
+ * Binds event handlers to the document for tracking a pending multi-select
+ * in response to a mousedown event occurring in a rendered block.
+ *
+ * @param {string} uid UID of the block where mousedown occurred.
+ *
+ * @returns {void}
+ */
onSelectionStart( uid ) {
if ( ! this.props.isSelectionEnabled ) {
return;
@@ -161,8 +184,6 @@ class BlockList extends Component {
// Capture scroll on all elements.
window.addEventListener( 'scroll', this.onScroll, true );
window.addEventListener( 'mouseup', this.onSelectionEnd );
-
- this.props.onStartMultiSelect();
}
onSelectionChange( uid ) {
@@ -183,6 +204,11 @@ class BlockList extends Component {
}
}
+ /**
+ * Handles a mouseup event to end the current cursor multi-selection.
+ *
+ * @returns {void}
+ */
onSelectionEnd() {
// Cancel throttled calls.
this.onPointerMove.cancel();
@@ -195,7 +221,11 @@ class BlockList extends Component {
window.removeEventListener( 'scroll', this.onScroll, true );
window.removeEventListener( 'mouseup', this.onSelectionEnd );
- this.props.onStopMultiSelect();
+ // We may or may not be in a multi-selection when mouseup occurs (e.g.
+ // an in-place mouse click), so only trigger stop if multi-selecting.
+ if ( this.props.isMultiSelecting ) {
+ this.props.onStopMultiSelect();
+ }
}
onShiftSelection( uid ) {
@@ -244,6 +274,7 @@ export default connect(
multiSelectedBlockUids: getMultiSelectedBlockUids( state ),
selectedBlock: getSelectedBlock( state ),
isSelectionEnabled: isSelectionEnabled( state ),
+ isMultiSelecting: isMultiSelecting( state ),
} ),
( dispatch ) => ( {
onStartMultiSelect() {
diff --git a/editor/components/block-list/style.scss b/editor/components/block-list/style.scss
index 14f1ea85090201..a9a24ff1df2125 100644
--- a/editor/components/block-list/style.scss
+++ b/editor/components/block-list/style.scss
@@ -405,7 +405,7 @@
position: absolute;
left: 0;
width: 100%;
- height: 44px;
+ height: 28px;
transition: 0.1s height;
transform: translateY( -50% );
}
diff --git a/editor/components/block-mover/index.js b/editor/components/block-mover/index.js
index 9c2d8964f41e70..3110a34ef0e73b 100644
--- a/editor/components/block-mover/index.js
+++ b/editor/components/block-mover/index.js
@@ -34,7 +34,7 @@ export function BlockMover( { onMoveUp, onMoveDown, isFirst, isLast, uids, block
}
tooltip={ __( 'Move Up' ) }
label={ getBlockMoverLabel(
uids.length,
@@ -49,7 +49,7 @@ export function BlockMover( { onMoveUp, onMoveDown, isFirst, isLast, uids, block
}
tooltip={ __( 'Move Down' ) }
label={ getBlockMoverLabel(
uids.length,
diff --git a/editor/components/block-mover/mover-label.js b/editor/components/block-mover/mover-label.js
index 1b09cce1742592..4e8e9d8fca0fb0 100644
--- a/editor/components/block-mover/mover-label.js
+++ b/editor/components/block-mover/mover-label.js
@@ -6,15 +6,16 @@ import { __, sprintf } from '@wordpress/i18n';
/**
* Return a label for the block movement controls depending on block position.
*
- * @param {number} selectedCount Number of blocks selected.
- * @param {string} type Block type - in the case of a single block, should
+ * @param {number} selectedCount Number of blocks selected.
+ * @param {string} type Block type - in the case of a single block, should
* define its 'type'. I.e. 'Text', 'Heading', 'Image' etc.
- * @param {number} firstIndex The index (position - 1) of the first block selected.
- * @param {boolean} isFirst This is the first block.
- * @param {boolean} isLast This is the last block.
- * @param {number} dir Direction of movement (> 0 is considered to be going
+ * @param {number} firstIndex The index (position - 1) of the first block selected.
+ * @param {boolean} isFirst This is the first block.
+ * @param {boolean} isLast This is the last block.
+ * @param {number} dir Direction of movement (> 0 is considered to be going
* down, < 0 is up).
- * @return {string} Label for the block movement controls.
+ *
+ * @returns {string} Label for the block movement controls.
*/
export function getBlockMoverLabel( selectedCount, type, firstIndex, isFirst, isLast, dir ) {
const position = ( firstIndex + 1 );
@@ -68,13 +69,14 @@ export function getBlockMoverLabel( selectedCount, type, firstIndex, isFirst, is
/**
* Return a label for the block movement controls depending on block position.
*
- * @param {number} selectedCount Number of blocks selected.
- * @param {number} firstIndex The index (position - 1) of the first block selected.
- * @param {boolean} isFirst This is the first block.
- * @param {boolean} isLast This is the last block.
- * @param {number} dir Direction of movement (> 0 is considered to be going
+ * @param {number} selectedCount Number of blocks selected.
+ * @param {number} firstIndex The index (position - 1) of the first block selected.
+ * @param {boolean} isFirst This is the first block.
+ * @param {boolean} isLast This is the last block.
+ * @param {number} dir Direction of movement (> 0 is considered to be going
* down, < 0 is up).
- * @return {string} Label for the block movement controls.
+ *
+ * @returns {string} Label for the block movement controls.
*/
export function getMultiBlockMoverLabel( selectedCount, firstIndex, isFirst, isLast, dir ) {
const position = ( firstIndex + 1 );
diff --git a/editor/components/block-mover/style.scss b/editor/components/block-mover/style.scss
index 95856486b2897c..e7c001ae04a9ef 100644
--- a/editor/components/block-mover/style.scss
+++ b/editor/components/block-mover/style.scss
@@ -16,11 +16,34 @@
pointer-events: none;
}
- .dashicon {
+ &:first-child {
+ margin-bottom: 4px;
+ }
+
+ // unstyle inherited icon button styles
+ &:not(:disabled):hover,
+ &:not(:disabled):active,
+ &:not(:disabled):focus {
+ box-shadow: none;
+ color: inherit;
+ }
+
+ // apply styles to SVG directly
+ svg {
display: block;
+ position: relative; // Fixing the Safari bug for ``s overflow
+ border-radius: 50%;
}
- &:first-child {
- margin-bottom: 4px;
+ &:not(:disabled):hover svg {
+ @include button-style__hover;
+ }
+
+ &:not(:disabled):active svg {
+ @include button-style__active;
+ }
+
+ &:not(:disabled):focus svg {
+ @include button-style__focus-active;
}
}
diff --git a/editor/components/block-mover/test/index.js b/editor/components/block-mover/test/index.js
index f6f785b24b7e14..e3aded6ce6e535 100644
--- a/editor/components/block-mover/test/index.js
+++ b/editor/components/block-mover/test/index.js
@@ -32,14 +32,12 @@ describe( 'BlockMover', () => {
expect( moveUp.props() ).toMatchObject( {
className: 'editor-block-mover__control',
onClick: undefined,
- icon: 'arrow-up-alt2',
label: 'Move 2 blocks from position 1 up by one place',
'aria-disabled': undefined,
} );
expect( moveDown.props() ).toMatchObject( {
className: 'editor-block-mover__control',
onClick: undefined,
- icon: 'arrow-down-alt2',
label: 'Move 2 blocks from position 1 down by one place',
'aria-disabled': undefined,
} );
diff --git a/editor/components/block-settings-menu/style.scss b/editor/components/block-settings-menu/style.scss
index f23fbce8ccee7c..994784f7d9ea2a 100644
--- a/editor/components/block-settings-menu/style.scss
+++ b/editor/components/block-settings-menu/style.scss
@@ -18,8 +18,11 @@
.editor-block-settings-menu__toggle {
border-radius: 50%;
padding: 3px;
- transform: rotate( 90deg );
width: auto;
+
+ .dashicon {
+ transform: rotate( 90deg );
+ }
}
.editor-block-settings-menu__control {
diff --git a/editor/components/inserter/group.js b/editor/components/inserter/group.js
index 8126a55159f6b2..359f3d2622d874 100644
--- a/editor/components/inserter/group.js
+++ b/editor/components/inserter/group.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { isEqual, find } from 'lodash';
+import { isEqual } from 'lodash';
/**
* WordPress dependencies
@@ -10,8 +10,8 @@ import { Component } from '@wordpress/element';
import { NavigableMenu } from '@wordpress/components';
import { BlockIcon } from '@wordpress/blocks';
-function deriveActiveBlocks( blocks ) {
- return blocks.filter( ( block ) => ! block.disabled );
+function deriveActiveItems( items ) {
+ return items.filter( ( item ) => ! item.isDisabled );
}
export default class InserterGroup extends Component {
@@ -20,61 +20,62 @@ export default class InserterGroup extends Component {
this.onNavigate = this.onNavigate.bind( this );
- this.activeBlocks = deriveActiveBlocks( this.props.blockTypes );
+ this.activeItems = deriveActiveItems( this.props.items );
this.state = {
- current: this.activeBlocks.length > 0 ? this.activeBlocks[ 0 ].name : null,
+ current: this.activeItems.length > 0 ? this.activeItems[ 0 ] : null,
};
}
componentWillReceiveProps( nextProps ) {
- if ( ! isEqual( this.props.blockTypes, nextProps.blockTypes ) ) {
- this.activeBlocks = deriveActiveBlocks( nextProps.blockTypes );
+ if ( ! isEqual( this.props.items, nextProps.items ) ) {
+ this.activeItems = deriveActiveItems( nextProps.items );
+
// Try and preserve any still valid selected state.
- const current = find( this.activeBlocks, { name: this.state.current } );
- if ( ! current ) {
+ const currentIsStillActive = this.state.current && this.activeItems.some( item =>
+ item.id === this.state.current.id
+ );
+
+ if ( ! currentIsStillActive ) {
this.setState( {
- current: this.activeBlocks.length > 0 ? this.activeBlocks[ 0 ].name : null,
+ current: this.activeItems.length > 0 ? this.activeItems[ 0 ] : null,
} );
}
}
}
- renderItem( block ) {
+ renderItem( item ) {
const { current } = this.state;
- const { selectBlock, bindReferenceNode } = this.props;
- const { disabled } = block;
+ const { onSelectItem } = this.props;
+
+ const isCurrent = current && current.id === item.id;
return (
onSelectItem( item ) }
+ tabIndex={ isCurrent || item.isDisabled ? null : '-1' }
+ disabled={ item.isDisabled }
>
-
- { block.title }
+
+ { item.title }
);
}
onNavigate( index ) {
- const { activeBlocks } = this;
- const dest = activeBlocks[ index ];
+ const { activeItems } = this;
+ const dest = activeItems[ index ];
if ( dest ) {
this.setState( {
- current: dest.name,
+ current: dest,
} );
}
}
render() {
- const { labelledBy, blockTypes } = this.props;
+ const { labelledBy, items } = this.props;
return (
- { blockTypes.map( this.renderItem, this ) }
+ { items.map( this.renderItem, this ) }
);
}
diff --git a/editor/components/inserter/index.js b/editor/components/inserter/index.js
index 51f385435f69b8..e77ec98c034eae 100644
--- a/editor/components/inserter/index.js
+++ b/editor/components/inserter/index.js
@@ -82,17 +82,12 @@ class Inserter extends Component {
) }
renderContent={ ( { onClose } ) => {
- const onInsert = ( name, initialAttributes ) => {
- onInsertBlock(
- name,
- initialAttributes,
- insertionPoint
- );
-
+ const onSelect = ( item ) => {
+ onInsertBlock( item, insertionPoint );
onClose();
};
- return ;
+ return ;
} }
/>
);
@@ -108,9 +103,9 @@ export default compose( [
};
},
( dispatch ) => ( {
- onInsertBlock( name, initialAttributes, position ) {
+ onInsertBlock( item, position ) {
dispatch( insertBlock(
- createBlock( name, initialAttributes ),
+ createBlock( item.name, item.initialAttributes ),
position
) );
},
diff --git a/editor/components/inserter/menu.js b/editor/components/inserter/menu.js
index be59c24a1815f5..600603c3f4956e 100644
--- a/editor/components/inserter/menu.js
+++ b/editor/components/inserter/menu.js
@@ -3,7 +3,6 @@
*/
import {
filter,
- find,
findIndex,
flow,
groupBy,
@@ -27,7 +26,7 @@ import {
withSpokenMessages,
withContext,
} from '@wordpress/components';
-import { getCategories, getBlockTypes } from '@wordpress/blocks';
+import { getCategories } from '@wordpress/blocks';
import { keycodes } from '@wordpress/utils';
/**
@@ -35,16 +34,16 @@ import { keycodes } from '@wordpress/utils';
*/
import './style.scss';
-import { getBlocks, getRecentlyUsedBlocks, getReusableBlocks } from '../../store/selectors';
+import { getInserterItems, getRecentInserterItems } from '../../store/selectors';
import { fetchReusableBlocks } from '../../store/actions';
import { default as InserterGroup } from './group';
-export const searchBlocks = ( blocks, searchTerm ) => {
+export const searchItems = ( items, searchTerm ) => {
const normalizedSearchTerm = searchTerm.toLowerCase().trim();
const matchSearch = ( string ) => string.toLowerCase().indexOf( normalizedSearchTerm ) !== -1;
- return blocks.filter( ( block ) =>
- matchSearch( block.title ) || some( block.keywords, matchSearch )
+ return items.filter( ( item ) =>
+ matchSearch( item.title ) || some( item.keywords, matchSearch )
);
};
@@ -62,11 +61,10 @@ export class InserterMenu extends Component {
tab: 'recent',
};
this.filter = this.filter.bind( this );
- this.searchBlocks = this.searchBlocks.bind( this );
- this.getBlocksForTab = this.getBlocksForTab.bind( this );
- this.sortBlocks = this.sortBlocks.bind( this );
- this.bindReferenceNode = this.bindReferenceNode.bind( this );
- this.selectBlock = this.selectBlock.bind( this );
+ this.searchItems = this.searchItems.bind( this );
+ this.getItemsForTab = this.getItemsForTab.bind( this );
+ this.sortItems = this.sortItems.bind( this );
+ this.selectItem = this.selectItem.bind( this );
this.tabScrollTop = { recent: 0, blocks: 0, embeds: 0 };
this.switchTab = this.switchTab.bind( this );
@@ -77,8 +75,8 @@ export class InserterMenu extends Component {
}
componentDidUpdate( prevProps, prevState ) {
- const searchResults = this.searchBlocks( this.getBlockTypes() );
- // Announce the blocks search results to screen readers.
+ const searchResults = this.searchItems( this.props.items );
+ // Announce the search results to screen readers.
if ( this.state.filterValue && !! searchResults.length ) {
this.props.debouncedSpeak( sprintf( _n(
'%d result found',
@@ -94,152 +92,91 @@ export class InserterMenu extends Component {
}
}
- isDisabledBlock( blockType ) {
- return blockType.useOnce && find( this.props.blocks, ( { name } ) => blockType.name === name );
- }
-
- bindReferenceNode( nodeName ) {
- return ( node ) => this.nodes[ nodeName ] = node;
- }
-
filter( event ) {
this.setState( {
filterValue: event.target.value,
} );
}
- selectBlock( block ) {
- return () => {
- this.props.onSelect( block.name, block.initialAttributes );
- this.setState( {
- filterValue: '',
- } );
- };
- }
-
- getStaticBlockTypes() {
- const { blockTypes } = this.props;
-
- // If all block types disabled, return empty set
- if ( ! blockTypes ) {
- return [];
- }
-
- // Block types that are marked as private should not appear in the inserter
- return getBlockTypes().filter( ( block ) => {
- if ( block.isPrivate ) {
- return false;
- }
-
- // Block types defined as either `true` or array:
- // - True: Allow
- // - Array: Check block name within whitelist
- return (
- ! Array.isArray( blockTypes ) ||
- includes( blockTypes, block.name )
- );
+ selectItem( item ) {
+ this.props.onSelect( item );
+ this.setState( {
+ filterValue: '',
} );
}
- getReusableBlockTypes() {
- const { reusableBlocks } = this.props;
-
- // Display reusable blocks that we've fetched in the inserter
- return reusableBlocks.map( ( reusableBlock ) => ( {
- name: 'core/block',
- initialAttributes: {
- ref: reusableBlock.id,
- },
- title: reusableBlock.title,
- icon: 'layout',
- category: 'reusable-blocks',
- } ) );
+ searchItems( items ) {
+ return searchItems( items, this.state.filterValue );
}
- getBlockTypes() {
- return [
- ...this.getStaticBlockTypes(),
- ...this.getReusableBlockTypes(),
- ];
- }
-
- searchBlocks( blockTypes ) {
- return searchBlocks( blockTypes, this.state.filterValue );
- }
+ getItemsForTab( tab ) {
+ const { items, recentItems } = this.props;
- getBlocksForTab( tab ) {
- const blockTypes = this.getBlockTypes();
- // if we're searching, use everything, otherwise just get the blocks visible in this tab
+ // If we're searching, use everything, otherwise just get the items visible in this tab
if ( this.state.filterValue ) {
- return blockTypes;
+ return items;
}
let predicate;
switch ( tab ) {
case 'recent':
- return filter( this.props.recentlyUsedBlocks,
- ( { name } ) => find( blockTypes, { name } ) );
+ return recentItems;
case 'blocks':
- predicate = ( block ) => block.category !== 'embed' && block.category !== 'reusable-blocks';
+ predicate = ( item ) => item.category !== 'embed' && item.category !== 'reusable-blocks';
break;
case 'embeds':
- predicate = ( block ) => block.category === 'embed';
+ predicate = ( item ) => item.category === 'embed';
break;
case 'saved':
- predicate = ( block ) => block.category === 'reusable-blocks';
+ predicate = ( item ) => item.category === 'reusable-blocks';
break;
}
- return filter( blockTypes, predicate );
+ return filter( items, predicate );
}
- sortBlocks( blockTypes ) {
+ sortItems( items ) {
if ( 'recent' === this.state.tab && ! this.state.filterValue ) {
- return blockTypes;
+ return items;
}
const getCategoryIndex = ( item ) => {
return findIndex( getCategories(), ( category ) => category.slug === item.category );
};
- return sortBy( blockTypes, getCategoryIndex );
+ return sortBy( items, getCategoryIndex );
}
- groupByCategory( blockTypes ) {
- return groupBy( blockTypes, ( blockType ) => blockType.category );
+ groupByCategory( items ) {
+ return groupBy( items, ( item ) => item.category );
}
- getVisibleBlocksByCategory( blockTypes ) {
+ getVisibleItemsByCategory( items ) {
return flow(
- this.searchBlocks,
- this.sortBlocks,
+ this.searchItems,
+ this.sortItems,
this.groupByCategory
- )( blockTypes );
+ )( items );
}
- renderBlocks( blockTypes, separatorSlug ) {
+ renderItems( items, separatorSlug ) {
const { instanceId } = this.props;
const labelledBy = separatorSlug === undefined ? null : `editor-inserter__separator-${ separatorSlug }-${ instanceId }`;
- const blockTypesInfo = blockTypes.map( ( blockType ) => (
- { ...blockType, disabled: this.isDisabledBlock( blockType ) }
- ) );
-
return (
);
}
- renderCategory( category, blockTypes ) {
+ renderCategory( category, items ) {
const { instanceId } = this.props;
- return blockTypes && (
+ return items && (
{ category.title }
- { this.renderBlocks( blockTypes, category.slug ) }
+ { this.renderItems( items, category.slug ) }
);
}
- renderCategories( visibleBlocksByCategory ) {
- if ( isEmpty( visibleBlocksByCategory ) ) {
+ renderCategories( visibleItemsByCategory ) {
+ if ( isEmpty( visibleItemsByCategory ) ) {
return (
{ __( 'No blocks found' ) }
@@ -263,7 +200,7 @@ export class InserterMenu extends Component {
}
return getCategories().map(
- ( category ) => this.renderCategory( category, visibleBlocksByCategory[ category.slug ] )
+ ( category ) => this.renderCategory( category, visibleItemsByCategory[ category.slug ] )
);
}
@@ -274,15 +211,15 @@ export class InserterMenu extends Component {
}
renderTabView( tab ) {
- const blocksForTab = this.getBlocksForTab( tab );
+ const itemsForTab = this.getItemsForTab( tab );
// If the Recent tab is selected, don't render category headers
if ( 'recent' === tab ) {
- return this.renderBlocks( blocksForTab );
+ return this.renderItems( itemsForTab );
}
// If the Saved tab is selected and we have no results, display a friendly message
- if ( 'saved' === tab && blocksForTab.length === 0 ) {
+ if ( 'saved' === tab && itemsForTab.length === 0 ) {
return (
{ __( 'No saved blocks.' ) }
@@ -290,16 +227,16 @@ export class InserterMenu extends Component {
);
}
- const visibleBlocksByCategory = this.getVisibleBlocksByCategory( blocksForTab );
+ const visibleItemsByCategory = this.getVisibleItemsByCategory( itemsForTab );
- // If our results have only blocks from one category, don't render category headers
- const categories = Object.keys( visibleBlocksByCategory );
+ // If our results have only items from one category, don't render category headers
+ const categories = Object.keys( visibleItemsByCategory );
if ( categories.length === 1 ) {
const [ soleCategory ] = categories;
- return this.renderBlocks( visibleBlocksByCategory[ soleCategory ] );
+ return this.renderItems( visibleItemsByCategory[ soleCategory ] );
}
- return this.renderCategories( visibleBlocksByCategory );
+ return this.renderCategories( visibleItemsByCategory );
}
// Passed to TabbableContainer, extending its event-handling logic
@@ -324,7 +261,7 @@ export class InserterMenu extends Component {
}
render() {
- const { instanceId } = this.props;
+ const { instanceId, items } = this.props;
const isSearching = this.state.filterValue;
return (
@@ -340,7 +277,6 @@ export class InserterMenu extends Component {
placeholder={ __( 'Search for a block' ) }
className="editor-inserter__search"
onChange={ this.filter }
- ref={ this.bindReferenceNode( 'search' ) }
/>
{ ! isSearching &&
- { this.renderCategories( this.getVisibleBlocksByCategory( this.getBlockTypes() ) ) }
+ { this.renderCategories( this.getVisibleItemsByCategory( items ) ) }
}
@@ -385,20 +321,23 @@ export class InserterMenu extends Component {
}
}
-const connectComponent = connect(
- ( state ) => {
+export default compose(
+ withContext( 'editor' )( ( settings ) => {
+ const { blockTypes } = settings;
+
return {
- recentlyUsedBlocks: getRecentlyUsedBlocks( state ),
- blocks: getBlocks( state ),
- reusableBlocks: getReusableBlocks( state ),
+ enabledBlockTypes: blockTypes,
};
- },
- { fetchReusableBlocks }
-);
-
-export default compose(
- connectComponent,
- withContext( 'editor' )( ( settings ) => pick( settings, 'blockTypes' ) ),
+ } ),
+ connect(
+ ( state, ownProps ) => {
+ return {
+ items: getInserterItems( state, ownProps.enabledBlockTypes ),
+ recentItems: getRecentInserterItems( state, ownProps.enabledBlockTypes ),
+ };
+ },
+ { fetchReusableBlocks }
+ ),
withSpokenMessages,
withInstanceId
)( InserterMenu );
diff --git a/editor/components/inserter/test/menu.js b/editor/components/inserter/test/menu.js
index 60dd6e4e9bc2d1..57a5be1f0b7d31 100644
--- a/editor/components/inserter/test/menu.js
+++ b/editor/components/inserter/test/menu.js
@@ -4,99 +4,97 @@
import { mount } from 'enzyme';
import { noop } from 'lodash';
-/**
- * WordPress dependencies
- */
-import { registerBlockType, unregisterBlockType, getBlockTypes } from '@wordpress/blocks';
-
/**
* Internal dependencies
*/
-import { InserterMenu, searchBlocks } from '../menu';
+import { InserterMenu, searchItems } from '../menu';
-const textBlock = {
+const textItem = {
+ id: 'core/text-block',
name: 'core/text-block',
+ initialAttributes: {},
title: 'Text',
- save: noop,
- edit: noop,
category: 'common',
+ isDisabled: false,
};
-const advancedTextBlock = {
+const advancedTextItem = {
+ id: 'core/advanced-text-block',
name: 'core/advanced-text-block',
+ initialAttributes: {},
title: 'Advanced Text',
- save: noop,
- edit: noop,
category: 'common',
+ isDisabled: false,
};
-const someOtherBlock = {
+const someOtherItem = {
+ id: 'core/some-other-block',
name: 'core/some-other-block',
+ initialAttributes: {},
title: 'Some Other Block',
- save: noop,
- edit: noop,
category: 'common',
+ isDisabled: false,
};
-const moreBlock = {
+const moreItem = {
+ id: 'core/more-block',
name: 'core/more-block',
+ initialAttributes: {},
title: 'More',
- save: noop,
- edit: noop,
category: 'layout',
- useOnce: 'true',
+ isDisabled: true,
};
-const youtubeBlock = {
+const youtubeItem = {
+ id: 'core-embed/youtube',
name: 'core-embed/youtube',
+ initialAttributes: {},
title: 'YouTube',
- save: noop,
- edit: noop,
category: 'embed',
keywords: [ 'google' ],
+ isDisabled: false,
};
-const textEmbedBlock = {
+const textEmbedItem = {
+ id: 'core-embed/a-text-embed',
name: 'core-embed/a-text-embed',
+ initialAttributes: {},
title: 'A Text Embed',
- save: noop,
- edit: noop,
category: 'embed',
+ isDisabled: false,
};
+const reusableItem = {
+ id: 'core/block/123',
+ name: 'core/block',
+ initialAttributes: { ref: 123 },
+ title: 'My reusable block',
+ category: 'reusable-blocks',
+ isDisabled: false,
+};
+
+const items = [
+ textItem,
+ advancedTextItem,
+ someOtherItem,
+ moreItem,
+ youtubeItem,
+ textEmbedItem,
+ reusableItem,
+];
+
describe( 'InserterMenu', () => {
// NOTE: Due to https://github.com/airbnb/enzyme/issues/1174, some of the selectors passed through to
// wrapper.find have had to be strengthened (and the filterWhere strengthened also), otherwise two
// results would be returned even though only one was in the DOM.
- const unregisterAllBlocks = () => {
- getBlockTypes().forEach( ( block ) => {
- unregisterBlockType( block.name );
- } );
- };
-
- afterEach( () => {
- unregisterAllBlocks();
- } );
-
- beforeEach( () => {
- unregisterAllBlocks();
- registerBlockType( textBlock.name, textBlock );
- registerBlockType( advancedTextBlock.name, advancedTextBlock );
- registerBlockType( someOtherBlock.name, someOtherBlock );
- registerBlockType( moreBlock.name, moreBlock );
- registerBlockType( youtubeBlock.name, youtubeBlock );
- registerBlockType( textEmbedBlock.name, textEmbedBlock );
- } );
-
it( 'should show the recent tab by default', () => {
const wrapper = mount(
{
expect( visibleBlocks ).toHaveLength( 0 );
} );
- it( 'should show no blocks if all block types disabled', () => {
+ it( 'should show nothing if there are no items', () => {
const wrapper = mount(
);
@@ -128,90 +124,81 @@ describe( 'InserterMenu', () => {
expect( visibleBlocks ).toHaveLength( 0 );
} );
- it( 'should show filtered block types', () => {
+ it( 'should show the recently used items in the recent tab', () => {
const wrapper = mount(
);
const visibleBlocks = wrapper.find( '.editor-inserter__block' );
- expect( visibleBlocks ).toHaveLength( 1 );
- expect( visibleBlocks.at( 0 ).text() ).toBe( 'Text' );
+ expect( visibleBlocks ).toHaveLength( 3 );
+ expect( visibleBlocks.at( 0 ).text() ).toBe( 'Advanced Text' );
+ expect( visibleBlocks.at( 1 ).text() ).toBe( 'Text' );
+ expect( visibleBlocks.at( 2 ).text() ).toBe( 'Some Other Block' );
} );
- it( 'should show the recently used blocks in the recent tab', () => {
+ it( 'should show items from the embed category in the embed tab', () => {
const wrapper = mount(
);
+ const embedTab = wrapper.find( '.editor-inserter__tab' )
+ .filterWhere( ( node ) => node.text() === 'Embeds' && node.name() === 'button' );
+ embedTab.simulate( 'click' );
+
+ const activeCategory = wrapper.find( '.editor-inserter__tab button.is-active' );
+ expect( activeCategory.text() ).toBe( 'Embeds' );
const visibleBlocks = wrapper.find( '.editor-inserter__block' );
- expect( visibleBlocks ).toHaveLength( 3 );
- expect( visibleBlocks.at( 0 ).childAt( 0 ).name() ).toBe( 'BlockIcon' );
- expect( visibleBlocks.at( 0 ).text() ).toBe( 'Advanced Text' );
+ expect( visibleBlocks ).toHaveLength( 2 );
+ expect( visibleBlocks.at( 0 ).text() ).toBe( 'YouTube' );
+ expect( visibleBlocks.at( 1 ).text() ).toBe( 'A Text Embed' );
} );
- it( 'should show blocks from the embed category in the embed tab', () => {
+ it( 'should show reusable items in the saved tab', () => {
const wrapper = mount(
);
const embedTab = wrapper.find( '.editor-inserter__tab' )
- .filterWhere( ( node ) => node.text() === 'Embeds' && node.name() === 'button' );
+ .filterWhere( ( node ) => node.text() === 'Saved' && node.name() === 'button' );
embedTab.simulate( 'click' );
const activeCategory = wrapper.find( '.editor-inserter__tab button.is-active' );
- expect( activeCategory.text() ).toBe( 'Embeds' );
+ expect( activeCategory.text() ).toBe( 'Saved' );
const visibleBlocks = wrapper.find( '.editor-inserter__block' );
- expect( visibleBlocks ).toHaveLength( 2 );
- expect( visibleBlocks.at( 0 ).text() ).toBe( 'YouTube' );
- expect( visibleBlocks.at( 1 ).text() ).toBe( 'A Text Embed' );
+ expect( visibleBlocks ).toHaveLength( 1 );
+ expect( visibleBlocks.at( 0 ).text() ).toBe( 'My reusable block' );
} );
- it( 'should show all blocks except embeds in the blocks tab', () => {
+ it( 'should show all items except embeds and reusable blocks in the blocks tab', () => {
const wrapper = mount(
);
const blocksTab = wrapper.find( '.editor-inserter__tab' )
@@ -229,40 +216,32 @@ describe( 'InserterMenu', () => {
expect( visibleBlocks.at( 3 ).text() ).toBe( 'More' );
} );
- it( 'should disable already used blocks with `usedOnce`', () => {
+ it( 'should disable items with `isDisabled`', () => {
const wrapper = mount(
);
- const blocksTab = wrapper.find( '.editor-inserter__tab' )
- .filterWhere( ( node ) => node.text() === 'Blocks' && node.name() === 'button' );
- blocksTab.simulate( 'click' );
- wrapper.update();
- const disabledBlocks = wrapper.find( '.editor-inserter__block[disabled]' );
+ const disabledBlocks = wrapper.find( '.editor-inserter__block[disabled=true]' );
expect( disabledBlocks ).toHaveLength( 1 );
expect( disabledBlocks.at( 0 ).text() ).toBe( 'More' );
} );
- it( 'should allow searching for blocks', () => {
+ it( 'should allow searching for items', () => {
const wrapper = mount(
);
wrapper.setState( { filterValue: 'text' } );
@@ -282,12 +261,10 @@ describe( 'InserterMenu', () => {
);
wrapper.setState( { filterValue: ' text' } );
@@ -303,18 +280,16 @@ describe( 'InserterMenu', () => {
} );
} );
-describe( 'searchBlocks', () => {
- it( 'should search blocks using the title ignoring case', () => {
- const blocks = [ textBlock, advancedTextBlock, moreBlock, youtubeBlock, textEmbedBlock ];
- expect( searchBlocks( blocks, 'TEXT' ) ).toEqual(
- [ textBlock, advancedTextBlock, textEmbedBlock ]
+describe( 'searchItems', () => {
+ it( 'should search items using the title ignoring case', () => {
+ expect( searchItems( items, 'TEXT' ) ).toEqual(
+ [ textItem, advancedTextItem, textEmbedItem ]
);
} );
- it( 'should search blocks using the keywords', () => {
- const blocks = [ textBlock, advancedTextBlock, moreBlock, youtubeBlock, textEmbedBlock ];
- expect( searchBlocks( blocks, 'GOOGL' ) ).toEqual(
- [ youtubeBlock ]
+ it( 'should search items using the keywords', () => {
+ expect( searchItems( items, 'GOOGL' ) ).toEqual(
+ [ youtubeItem ]
);
} );
} );
diff --git a/editor/components/meta-boxes/meta-boxes-area/index.js b/editor/components/meta-boxes/meta-boxes-area/index.js
index a4d71ac27adbd1..be78633428b7f2 100644
--- a/editor/components/meta-boxes/meta-boxes-area/index.js
+++ b/editor/components/meta-boxes/meta-boxes-area/index.js
@@ -1,15 +1,12 @@
/**
* External dependencies
*/
-import { isEqual } from 'lodash';
import classnames from 'classnames';
import { connect } from 'react-redux';
-import jQuery from 'jquery';
/**
* WordPress dependencies
*/
-import { addQueryArgs } from '@wordpress/url';
import { Component } from '@wordpress/element';
import { Spinner } from '@wordpress/components';
@@ -17,137 +14,76 @@ import { Spinner } from '@wordpress/components';
* Internal dependencies
*/
import './style.scss';
-import { handleMetaBoxReload, metaBoxStateChanged, metaBoxLoaded } from '../../../store/actions';
-import { getMetaBox, isSavingPost } from '../../../store/selectors';
+import { isSavingMetaBoxes } from '../../../store/selectors';
class MetaBoxesArea extends Component {
+ /**
+ * @inheritdoc
+ */
constructor() {
super( ...arguments );
-
- this.state = {
- loading: false,
- };
- this.originalFormData = '';
- this.bindNode = this.bindNode.bind( this );
- this.checkState = this.checkState.bind( this );
- }
-
- bindNode( node ) {
- this.node = node;
+ this.bindContainerNode = this.bindContainerNode.bind( this );
}
+ /**
+ * @inheritdoc
+ */
componentDidMount() {
- this.mounted = true;
- this.fetchMetaboxes();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- this.unbindFormEvents();
- document.querySelector( '#metaboxes' ).appendChild( this.form );
- }
-
- unbindFormEvents() {
+ this.form = document.querySelector( '.metabox-location-' + this.props.location );
if ( this.form ) {
- this.form.removeEventListener( 'change', this.checkState );
- this.form.removeEventListener( 'input', this.checkState );
+ this.container.appendChild( this.form );
}
}
- componentWillReceiveProps( nextProps ) {
- if ( nextProps.isUpdating && ! this.props.isUpdating ) {
- this.setState( { loading: true } );
- const { location } = nextProps;
- const headers = new window.Headers();
- const fetchOptions = {
- method: 'POST',
- headers,
- body: new window.FormData( this.form ),
- credentials: 'include',
- };
-
- // Save the metaboxes
- window.fetch( addQueryArgs( window._wpMetaBoxUrl, { meta_box: location } ), fetchOptions )
- .then( () => {
- if ( ! this.mounted ) {
- return false;
- }
- this.setState( { loading: false } );
- this.props.metaBoxReloaded( location );
- } );
+ /**
+ * Get the meta box location form from the original location.
+ */
+ componentWillUnmount() {
+ if ( this.form ) {
+ document.querySelector( '#metaboxes' ).appendChild( this.form );
}
}
- fetchMetaboxes() {
- const { location } = this.props;
- this.form = document.querySelector( '.metabox-location-' + location );
- this.node.appendChild( this.form );
- this.form.onSubmit = ( event ) => event.preventDefault();
- this.originalFormData = this.getFormData();
- this.form.addEventListener( 'change', this.checkState );
- this.form.addEventListener( 'input', this.checkState );
- this.props.metaBoxLoaded( location );
- }
-
- getFormData() {
- return jQuery( this.form ).serialize();
- }
-
- checkState() {
- const { loading } = this.state;
- const { isDirty, changedMetaBoxState, location } = this.props;
-
- const newIsDirty = ! isEqual( this.originalFormData, this.getFormData() );
-
- /**
- * If we are not updating, then if dirty and equal to original, then set not dirty.
- * If we are not updating, then if not dirty and not equal to original, set as dirty.
- */
- if ( ! loading && isDirty !== newIsDirty ) {
- changedMetaBoxState( location, newIsDirty );
- }
+ /**
+ * Binds the metabox area container node.
+ *
+ * @param {Element} node DOM Node.
+ */
+ bindContainerNode( node ) {
+ this.container = node;
}
+ /**
+ * @inheritdoc
+ */
render() {
- const { location } = this.props;
- const { loading } = this.state;
+ const { location, isSaving } = this.props;
const classes = classnames(
'editor-meta-boxes-area',
`is-${ location }`,
{
- 'is-loading': loading,
+ 'is-loading': isSaving,
}
);
return (
- { loading &&
}
-
+ { isSaving &&
}
+
);
}
}
-function mapStateToProps( state, ownProps ) {
- const metaBox = getMetaBox( state, ownProps.location );
- const { isDirty, isUpdating } = metaBox;
-
- return {
- isDirty,
- isUpdating,
- isPostSaving: isSavingPost( state ) ? true : false,
- };
-}
-
-function mapDispatchToProps( dispatch ) {
+/**
+ * @inheritdoc
+ */
+function mapStateToProps( state ) {
return {
- // Used to set the reference to the MetaBox in redux, fired when the component mounts.
- metaBoxReloaded: ( location ) => dispatch( handleMetaBoxReload( location ) ),
- changedMetaBoxState: ( location, hasChanged ) => dispatch( metaBoxStateChanged( location, hasChanged ) ),
- metaBoxLoaded: ( location ) => dispatch( metaBoxLoaded( location ) ),
+ isSaving: isSavingMetaBoxes( state ),
};
}
-export default connect( mapStateToProps, mapDispatchToProps )( MetaBoxesArea );
+export default connect( mapStateToProps )( MetaBoxesArea );
diff --git a/editor/components/post-featured-image/index.js b/editor/components/post-featured-image/index.js
index 02b74dbfef5e28..60324a284815ef 100644
--- a/editor/components/post-featured-image/index.js
+++ b/editor/components/post-featured-image/index.js
@@ -9,7 +9,7 @@ import { get } from 'lodash';
*/
import { __ } from '@wordpress/i18n';
import { Button, Spinner, ResponsiveWrapper, withAPIData } from '@wordpress/components';
-import { MediaUploadButton } from '@wordpress/blocks';
+import { MediaUpload } from '@wordpress/blocks';
import { compose } from '@wordpress/element';
/**
@@ -31,23 +31,25 @@ function PostFeaturedImage( { featuredImageId, onUpdateImage, onRemoveImage, med
{ !! featuredImageId &&
-
- { media && !! media.data &&
-
-
-
- }
- { media && media.isLoading && }
-
+ render={ ( { open } ) => (
+
+ { media && !! media.data &&
+
+
+
+ }
+ { media && media.isLoading && }
+
+ ) }
+ />
}
{ !! featuredImageId && media && ! media.isLoading &&
@@ -55,15 +57,17 @@ function PostFeaturedImage( { featuredImageId, onUpdateImage, onRemoveImage, med
}
{ ! featuredImageId &&
-
- { postLabel.set_featured_image || DEFAULT_SET_FEATURE_IMAGE_LABEL }
-
+ render={ ( { open } )=>(
+
+ { postLabel.set_featured_image || DEFAULT_SET_FEATURE_IMAGE_LABEL }
+
+ ) }
+ />
}
{ !! featuredImageId &&
diff --git a/editor/components/post-publish-button/style.scss b/editor/components/post-publish-button/style.scss
index 0bec4c9ca20ecf..ecf96ec0bff8e4 100644
--- a/editor/components/post-publish-button/style.scss
+++ b/editor/components/post-publish-button/style.scss
@@ -3,13 +3,13 @@
position: relative;
// These styles overrides the disabled state with the button primary styles
+ opacity: 1;
background: none !important;
- border-color: #0073aa #006799 #006799 !important;
- box-shadow: 0 1px 0 #006799 !important;
- color: #fff !important;
text-decoration: none !important;
- text-shadow: 0 -1px 1px #006799, 1px 0 1px #006799, 0 1px 1px #006799, -1px 0 1px #006799 !important;
- opacity: 1;
+ color: $white !important;
+ border-color: $blue-wordpress $blue-wordpress-700 $blue-wordpress-700 !important;
+ box-shadow: 0 1px 0 $blue-wordpress-700 !important;
+ text-shadow: 0 -1px 1px $blue-wordpress-700, 1px 0 1px $blue-wordpress-700, 0 1px 1px $blue-wordpress-700, -1px 0 1px $blue-wordpress-700 !important;
// End of the overriding
&:hover {
diff --git a/editor/components/post-saved-state/index.js b/editor/components/post-saved-state/index.js
index e11607e6eb971f..5339e88138ceb6 100644
--- a/editor/components/post-saved-state/index.js
+++ b/editor/components/post-saved-state/index.js
@@ -24,9 +24,16 @@ import {
isEditedPostSaveable,
getCurrentPost,
getEditedPostAttribute,
+ hasMetaBoxes,
} from '../../store/selectors';
-export function PostSavedState( { isNew, isPublished, isDirty, isSaving, isSaveable, status, onStatusChange, onSave } ) {
+/**
+ * Component showing whether the post is saved or not and displaying save links.
+ *
+ * @param {Object} Props Component Props.
+ * @returns {WPElement} WordPress Element.
+ */
+export function PostSavedState( { hasActiveMetaboxes, isNew, isPublished, isDirty, isSaving, isSaveable, status, onStatusChange, onSave } ) {
const className = 'editor-post-saved-state';
if ( isSaving ) {
@@ -45,7 +52,7 @@ export function PostSavedState( { isNew, isPublished, isDirty, isSaving, isSavea
return null;
}
- if ( ! isNew && ! isDirty ) {
+ if ( ! isNew && ! isDirty && ! hasActiveMetaboxes ) {
return (
@@ -79,6 +86,7 @@ export default connect(
isSaving: isSavingPost( state ),
isSaveable: isEditedPostSaveable( state ),
status: getEditedPostAttribute( state, 'status' ),
+ hasActiveMetaboxes: hasMetaBoxes( state ),
} ),
{
onStatusChange: ( status ) => editPost( { status } ),
diff --git a/editor/components/post-taxonomies/flat-term-selector.js b/editor/components/post-taxonomies/flat-term-selector.js
index b6d99a1783ebac..4b414f28bfd2c2 100644
--- a/editor/components/post-taxonomies/flat-term-selector.js
+++ b/editor/components/post-taxonomies/flat-term-selector.js
@@ -106,8 +106,13 @@ class FlatTermSelector extends Component {
.then( resolve, ( xhr ) => {
const errorCode = xhr.responseJSON && xhr.responseJSON.code;
if ( errorCode === 'term_exists' ) {
- return new Model( { id: xhr.responseJSON.data } )
- .fetch().then( resolve, reject );
+ // search the new category created since last fetch
+ this.addRequest = new Model().fetch(
+ { data: { ...DEFAULT_QUERY, search: termName } }
+ );
+ return this.addRequest.then( searchResult => {
+ resolve( find( searchResult, result => result.name === termName ) );
+ }, reject );
}
reject( xhr );
} );
diff --git a/editor/components/unsaved-changes-warning/index.js b/editor/components/unsaved-changes-warning/index.js
index 4e85b4c9f04b02..1800ff515d0755 100644
--- a/editor/components/unsaved-changes-warning/index.js
+++ b/editor/components/unsaved-changes-warning/index.js
@@ -2,6 +2,8 @@
* External dependencies
*/
import { connect } from 'react-redux';
+import { some } from 'lodash';
+import jQuery from 'jquery';
/**
* WordPress dependencies
@@ -12,29 +14,52 @@ import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
-import { isEditedPostDirty } from '../../store/selectors';
+import { isEditedPostDirty, getMetaBoxes } from '../../store/selectors';
+import { getMetaBoxContainer } from '../../edit-post/meta-boxes';
class UnsavedChangesWarning extends Component {
+ /**
+ * @inheritdoc
+ */
constructor() {
super( ...arguments );
this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind( this );
}
+ /**
+ * @inheritdoc
+ */
componentDidMount() {
window.addEventListener( 'beforeunload', this.warnIfUnsavedChanges );
}
+ /**
+ * @inheritdoc
+ */
componentWillUnmount() {
window.removeEventListener( 'beforeunload', this.warnIfUnsavedChanges );
}
+ /**
+ * Warns the user if there are unsaved changes before leaving the editor.
+ *
+ * @param {Event} event Event Object.
+ * @returns {string?} Warning message.
+ */
warnIfUnsavedChanges( event ) {
- if ( this.props.isDirty ) {
+ const areMetaBoxesDirty = some( this.props.metaBoxes, ( metaBox, location ) => {
+ return metaBox.isActive &&
+ jQuery( getMetaBoxContainer( location ) ).serialize() !== metaBox.data;
+ } );
+ if ( this.props.isDirty || areMetaBoxesDirty ) {
event.returnValue = __( 'You have unsaved changes. If you proceed, they will be lost.' );
return event.returnValue;
}
}
+ /**
+ * @inheritdoc
+ */
render() {
return null;
}
@@ -43,5 +68,6 @@ class UnsavedChangesWarning extends Component {
export default connect(
( state ) => ( {
isDirty: isEditedPostDirty( state ),
+ metaBoxes: getMetaBoxes( state ),
} )
)( UnsavedChangesWarning );
diff --git a/editor/edit-post/header/editor-actions/index.js b/editor/edit-post/header/editor-actions/index.js
new file mode 100644
index 00000000000000..a1b424d262b12c
--- /dev/null
+++ b/editor/edit-post/header/editor-actions/index.js
@@ -0,0 +1,18 @@
+/**
+ * WordPress dependencies
+ */
+import { MenuItemsGroup } from '@wordpress/components';
+import { applyFilters } from '@wordpress/hooks';
+import { __ } from '@wordpress/i18n';
+
+export default function EditorActions() {
+ const tools = applyFilters( 'editor.EditorActions.tools', [] );
+ return tools.length ? (
+
+ { tools }
+
+ ) : null;
+}
diff --git a/editor/edit-post/header/ellipsis-menu/index.js b/editor/edit-post/header/ellipsis-menu/index.js
index d78f028d0feb61..be3d8d6ba3e8ed 100644
--- a/editor/edit-post/header/ellipsis-menu/index.js
+++ b/editor/edit-post/header/ellipsis-menu/index.js
@@ -10,6 +10,7 @@ import { IconButton, Dropdown } from '@wordpress/components';
import './style.scss';
import ModeSwitcher from '../mode-switcher';
import FixedToolbarToggle from '../fixed-toolbar-toggle';
+import EditorActions from '../editor-actions';
const element = (
+
+
) }
/>
diff --git a/editor/edit-post/header/fixed-toolbar-toggle/index.js b/editor/edit-post/header/fixed-toolbar-toggle/index.js
index dcc620dab7432e..d9f2d2af3c118c 100644
--- a/editor/edit-post/header/fixed-toolbar-toggle/index.js
+++ b/editor/edit-post/header/fixed-toolbar-toggle/index.js
@@ -21,7 +21,7 @@ function FeatureToggle( { onToggle, active, onMobile } ) {
}
return (
{
+ const area = document.querySelector( `.editor-meta-boxes-area.is-${ location } .metabox-location-${ location }` );
+ if ( area ) {
+ return area;
+ }
+
+ return document.querySelector( '#metaboxes .metabox-location-' + location );
+};
diff --git a/editor/edit-post/modes/visual-editor/style.scss b/editor/edit-post/modes/visual-editor/style.scss
index 762302d77e21c1..50b99c2d5fd404 100644
--- a/editor/edit-post/modes/visual-editor/style.scss
+++ b/editor/edit-post/modes/visual-editor/style.scss
@@ -41,7 +41,7 @@
// like for example an image block that receives arrowkey focus.
.editor-visual-editor .editor-block-list__block:not( .is-selected ) .editor-block-list__block-edit {
box-shadow: 0 0 0 0 $white, 0 0 0 0 $dark-gray-900;
- transition: .1s box-shadow;
+ transition: .1s box-shadow .05s;
&:focus {
box-shadow: 0 0 0 1px $white, 0 0 0 3px $dark-gray-900;
diff --git a/editor/hooks/copy-content/index.js b/editor/hooks/copy-content/index.js
new file mode 100644
index 00000000000000..14ba4851508620
--- /dev/null
+++ b/editor/hooks/copy-content/index.js
@@ -0,0 +1,38 @@
+/**
+ * WordPress dependencies
+ */
+import { ClipboardButton, withState } from '@wordpress/components';
+import { compose } from '@wordpress/element';
+import { query } from '@wordpress/data';
+import { addFilter } from '@wordpress/hooks';
+import { __ } from '@wordpress/i18n';
+
+function CopyContentButton( { editedPostContent, hasCopied, setState } ) {
+ return (
+ setState( { hasCopied: true } ) }
+ onFinishCopy={ () => setState( { hasCopied: false } ) }
+ >
+ { hasCopied ?
+ __( 'Copied!' ) :
+ __( 'Copy All Content' ) }
+
+ );
+}
+
+const Enhanced = compose(
+ query( ( select ) => ( {
+ editedPostContent: select( 'core/editor', 'getEditedPostContent' ),
+ } ) ),
+ withState( { hasCopied: false } )
+)( CopyContentButton );
+
+const buttonElement = ;
+
+addFilter(
+ 'editor.EditorActions.tools',
+ 'core/copy-content/button',
+ ( children ) => [ ...children, buttonElement ]
+);
diff --git a/editor/hooks/index.js b/editor/hooks/index.js
new file mode 100644
index 00000000000000..540437a892858f
--- /dev/null
+++ b/editor/hooks/index.js
@@ -0,0 +1,4 @@
+/**
+ * Internal dependencies
+ */
+import './copy-content';
diff --git a/editor/index.js b/editor/index.js
index 2cd98d2b73bfb6..49767173146815 100644
--- a/editor/index.js
+++ b/editor/index.js
@@ -13,6 +13,7 @@ import { settings as dateSettings } from '@wordpress/date';
/**
* Internal dependencies
*/
+import './hooks';
import './assets/stylesheets/main.scss';
import Layout from './edit-post/layout';
import { EditorProvider, ErrorBoundary } from './components';
@@ -38,7 +39,8 @@ if ( dateSettings.timezone.string ) {
}
/**
- * Configure heartbeat to refresh the wp-api nonce, keeping the editor authorization intact.
+ * Configure heartbeat to refresh the wp-api nonce, keeping the editor
+ * authorization intact.
*/
window.jQuery( document ).on( 'heartbeat-tick', ( event, response ) => {
if ( response[ 'rest-nonce' ] ) {
@@ -51,8 +53,8 @@ window.jQuery( document ).on( 'heartbeat-tick', ( event, response ) => {
* an unhandled error occurs, replacing previously mounted editor element using
* an initial state from prior to the crash.
*
- * @param {Element} target DOM node in which editor is rendered
- * @param {?Object} settings Editor settings object
+ * @param {Element} target DOM node in which editor is rendered.
+ * @param {?Object} settings Editor settings object.
*/
export function recreateEditorInstance( target, settings ) {
unmountComponentAtNode( target );
@@ -75,10 +77,11 @@ export function recreateEditorInstance( target, settings ) {
* The return value of this function is not necessary if we change where we
* call createEditorInstance(). This is due to metaBox timing.
*
- * @param {String} id Unique identifier for editor instance
- * @param {Object} post API entity for post to edit
- * @param {?Object} settings Editor settings object
- * @return {Object} Editor interface
+ * @param {string} id Unique identifier for editor instance.
+ * @param {Object} post API entity for post to edit.
+ * @param {?Object} settings Editor settings object.
+ *
+ * @returns {Object} Editor interface.
*/
export function createEditorInstance( id, post, settings ) {
const target = document.getElementById( id );
diff --git a/editor/store/actions.js b/editor/store/actions.js
index 0e4b6cd723edfc..4f79030ca0c3b8 100644
--- a/editor/store/actions.js
+++ b/editor/store/actions.js
@@ -8,9 +8,10 @@ import { partial, castArray } from 'lodash';
* Returns an action object used in signalling that editor has initialized with
* the specified post object and editor settings.
*
- * @param {Object} post Post object
- * @param {Object} settings Editor settings object
- * @return {Object} Action object
+ * @param {Object} post Post object.
+ * @param {Object} settings Editor settings object.
+ *
+ * @returns {Object} Action object.
*/
export function setupEditor( post, settings ) {
return {
@@ -24,8 +25,9 @@ export function setupEditor( post, settings ) {
* Returns an action object used in signalling that the latest version of the
* post has been received, either by initialization or save.
*
- * @param {Object} post Post object
- * @return {Object} Action object
+ * @param {Object} post Post object.
+ *
+ * @returns {Object} Action object.
*/
export function resetPost( post ) {
return {
@@ -38,8 +40,9 @@ export function resetPost( post ) {
* Returns an action object used in signalling that editor has initialized as a
* new post with specified edits which should be considered non-dirtying.
*
- * @param {Object} edits Edited attributes object
- * @return {Object} Action object
+ * @param {Object} edits Edited attributes object.
+ *
+ * @returns {Object} Action object.
*/
export function setupNewPost( edits ) {
return {
@@ -53,8 +56,9 @@ export function setupNewPost( edits ) {
* 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
+ * @param {Array} blocks Array of blocks.
+ *
+ * @returns {Object} Action object.
*/
export function resetBlocks( blocks ) {
return {
@@ -64,12 +68,13 @@ export function resetBlocks( blocks ) {
}
/**
- * Returns an action object used in signalling that the block attributes with the
- * specified UID has been updated.
+ * Returns an action object used in signalling that the block attributes with
+ * the specified UID has been updated.
*
- * @param {String} uid Block UID
- * @param {Object} attributes Block attributes to be merged
- * @return {Object} Action object
+ * @param {string} uid Block UID.
+ * @param {Object} attributes Block attributes to be merged.
+ *
+ * @returns {Object} Action object.
*/
export function updateBlockAttributes( uid, attributes ) {
return {
@@ -83,9 +88,10 @@ export function updateBlockAttributes( uid, attributes ) {
* Returns an action object used in signalling that the block with the
* specified UID has been updated.
*
- * @param {String} uid Block UID
- * @param {Object} updates Block attributes to be merged
- * @return {Object} Action object
+ * @param {string} uid Block UID.
+ * @param {Object} updates Block attributes to be merged.
+ *
+ * @returns {Object} Action object.
*/
export function updateBlock( uid, updates ) {
return {
@@ -137,10 +143,12 @@ export function clearSelectedBlock() {
}
/**
- * Returns an action object that enables or disables block selection
+ * Returns an action object that enables or disables block selection.
*
- * @param {boolean} [isSelectionEnabled=true] Whether block selection should be enabled
- * @return {Object} Action object
+ * @param {boolean} [isSelectionEnabled=true] Whether block selection should
+ * be enabled.
+
+ * @returns {Object} Action object.
*/
export function toggleSelection( isSelectionEnabled = true ) {
return {
@@ -153,9 +161,10 @@ export function toggleSelection( isSelectionEnabled = true ) {
* Returns an action object signalling that a blocks should be replaced with
* one or more replacement blocks.
*
- * @param {(String|String[])} uids Block UID(s) to replace
- * @param {(Object|Object[])} blocks Replacement block(s)
- * @return {Object} Action object
+ * @param {(string|string[])} uids Block UID(s) to replace.
+ * @param {(Object|Object[])} blocks Replacement block(s).
+ *
+ * @returns {Object} Action object.
*/
export function replaceBlocks( uids, blocks ) {
return {
@@ -169,9 +178,10 @@ export function replaceBlocks( uids, blocks ) {
* Returns an action object signalling that a single block should be replaced
* with one or more replacement blocks.
*
- * @param {(String|String[])} uid Block UID(s) to replace
- * @param {(Object|Object[])} block Replacement block(s)
- * @return {Object} Action object
+ * @param {(string|string[])} uid Block UID(s) to replace.
+ * @param {(Object|Object[])} block Replacement block(s).
+ *
+ * @returns {Object} Action object.
*/
export function replaceBlock( uid, block ) {
return replaceBlocks( uid, block );
@@ -190,10 +200,11 @@ export function insertBlocks( blocks, position ) {
}
/**
- * Returns an action object showing the insertion point at a given index
+ * Returns an action object showing the insertion point at a given index.
*
- * @param {Number?} index Index of the insertion point
- * @return {Object} Action object
+ * @param {Number?} index Index of the insertion point.
+ *
+ * @returns {Object} Action object.
*/
export function showInsertionPoint( index ) {
return {
@@ -203,9 +214,9 @@ export function showInsertionPoint( index ) {
}
/**
- * Returns an action object hiding the insertion point
+ * Returns an action object hiding the insertion point.
*
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function hideInsertionPoint() {
return {
@@ -244,7 +255,7 @@ export function mergeBlocks( blockA, blockB ) {
/**
* Returns an action object used in signalling that the post should autosave.
*
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function autosave() {
return {
@@ -256,7 +267,7 @@ export function autosave() {
* Returns an action object used in signalling that undo history should
* restore last popped state.
*
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function redo() {
return { type: 'REDO' };
@@ -265,7 +276,7 @@ export function redo() {
/**
* Returns an action object used in signalling that undo history should pop.
*
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function undo() {
return { type: 'UNDO' };
@@ -275,8 +286,9 @@ export function undo() {
* Returns an action object used in signalling that the blocks
* corresponding to the specified UID set are to be removed.
*
- * @param {String[]} uids Block UIDs
- * @return {Object} Action object
+ * @param {string[]} uids Block UIDs.
+ *
+ * @returns {Object} Action object.
*/
export function removeBlocks( uids ) {
return {
@@ -289,18 +301,20 @@ export function removeBlocks( uids ) {
* Returns an action object used in signalling that the block with the
* specified UID is to be removed.
*
- * @param {String} uid Block UID
- * @return {Object} Action object
+ * @param {string} uid Block UID.
+ *
+ * @returns {Object} Action object.
*/
export function removeBlock( uid ) {
return removeBlocks( [ uid ] );
}
/**
- * Returns an action object used to toggle the block editing mode (visual/html)
+ * Returns an action object used to toggle the block editing mode (visual/html).
*
- * @param {String} uid Block UID
- * @return {Object} Action object
+ * @param {string} uid Block UID.
+ *
+ * @returns {Object} Action object.
*/
export function toggleBlockMode( uid ) {
return {
@@ -312,7 +326,7 @@ export function toggleBlockMode( uid ) {
/**
* Returns an action object used in signalling that the user has begun to type.
*
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function startTyping() {
return {
@@ -323,7 +337,7 @@ export function startTyping() {
/**
* Returns an action object used in signalling that the user has stopped typing.
*
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function stopTyping() {
return {
@@ -332,11 +346,14 @@ export function stopTyping() {
}
/**
- * Returns an action object used in signalling that the user toggled the sidebar
+ * Returns an action object used in signalling that the user toggled the
+ * sidebar.
+ *
+ * @param {string} sidebar Name of the sidebar to toggle
+ * (desktop, mobile or publish).
+ * @param {boolean?} forcedValue Force a sidebar state.
*
- * @param {String} sidebar Name of the sidebar to toggle (desktop, mobile or publish)
- * @param {Boolean?} forcedValue Force a sidebar state
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function toggleSidebar( sidebar, forcedValue ) {
return {
@@ -347,10 +364,12 @@ export function toggleSidebar( sidebar, forcedValue ) {
}
/**
- * Returns an action object used in signalling that the user switched the active sidebar tab panel
+ * Returns an action object used in signalling that the user switched the active
+ * sidebar tab panel.
*
- * @param {String} panel The panel name
- * @return {Object} Action object
+ * @param {string} panel The panel name.
+ *
+ * @returns {Object} Action object.
*/
export function setActivePanel( panel ) {
return {
@@ -360,10 +379,12 @@ export function setActivePanel( panel ) {
}
/**
- * Returns an action object used in signalling that the user toggled a sidebar panel
+ * Returns an action object used in signalling that the user toggled a
+ * sidebar panel.
+ *
+ * @param {string} panel The panel name.
*
- * @param {String} panel The panel name
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function toggleSidebarPanel( panel ) {
return {
@@ -373,15 +394,15 @@ export function toggleSidebarPanel( panel ) {
}
/**
- * Returns an action object used to create a notice
+ * Returns an action object used to create a notice.
*
- * @param {String} status The notice status
- * @param {WPElement} content The notice content
- * @param {?Object} options The notice options. Available options:
+ * @param {string} status The notice status.
+ * @param {WPElement} content The notice content.
+ * @param {?Object} options The notice options. Available options:
* `id` (string; default auto-generated)
- * `isDismissible` (boolean; default `true`)
+ * `isDismissible` (boolean; default `true`).
*
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function createNotice( status, content, options = {} ) {
const {
@@ -402,11 +423,11 @@ export function createNotice( status, content, options = {} ) {
}
/**
- * Returns an action object used to remove a notice
+ * Returns an action object used to remove a notice.
*
- * @param {String} id The notice id
+ * @param {string} id The notice id.
*
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function removeNotice( id ) {
return {
@@ -422,12 +443,13 @@ export function removeNotice( id ) {
* area is empty, this will set the store state to indicate that React should
* not render the meta box area.
*
- * Example: metaBoxes = { side: true, normal: false }
+ * Example: metaBoxes = { side: true, normal: false }.
+ *
* This indicates that the sidebar has a meta box but the normal area does not.
*
* @param {Object} metaBoxes Whether meta box locations are active.
*
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function initializeMetaBoxState( metaBoxes ) {
return {
@@ -437,69 +459,47 @@ export function initializeMetaBoxState( metaBoxes ) {
}
/**
- * Returns an action object used to signify that a meta box finished reloading.
- *
- * @param {String} location Location of meta box: 'normal', 'side' or 'advanced'.
- *
- * @return {Object} Action object
- */
-export function handleMetaBoxReload( location ) {
- return {
- type: 'HANDLE_META_BOX_RELOAD',
- location,
- };
-}
-
-/**
- * Returns an action object used to signify that a meta box finished loading.
- *
- * @param {String} location Location of meta box: 'normal', 'side' or 'advanced'.
+ * Returns an action object used to request meta box update.
*
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
-export function metaBoxLoaded( location ) {
+export function requestMetaBoxUpdates() {
return {
- type: 'META_BOX_LOADED',
- location,
+ type: 'REQUEST_META_BOX_UPDATES',
};
}
/**
- * Returns an action object used to request meta box update.
+ * Returns an action object used signal a successfull meta nox update.
*
- * @param {Array} locations Locations of meta boxes: ['normal', 'side', 'advanced' ].
- *
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
-export function requestMetaBoxUpdates( locations ) {
+export function metaBoxUpdatesSuccess() {
return {
- type: 'REQUEST_META_BOX_UPDATES',
- locations,
+ type: 'META_BOX_UPDATES_SUCCESS',
};
}
/**
- * Returns an action object used to set meta box state changed.
- *
- * @param {String} location Location of meta box: 'normal', 'side' or 'advanced'.
- * @param {Boolean} hasChanged Whether the meta box has changed.
+ * Returns an action object used set the saved meta boxes data.
+ * This is used to check if the meta boxes have been touched when leaving the editor.
*
- * @return {Object} Action object
+ * @param {Object} dataPerLocation Meta Boxes Data per location.
+ * @returns {Object} Action object.
*/
-export function metaBoxStateChanged( location, hasChanged ) {
+export function setMetaBoxSavedData( dataPerLocation ) {
return {
- type: 'META_BOX_STATE_CHANGED',
- location,
- hasChanged,
+ type: 'META_BOX_SET_SAVED_DATA',
+ dataPerLocation,
};
}
/**
- * Returns an action object used to toggle a feature flag
+ * Returns an action object used to toggle a feature flag.
*
- * @param {String} feature Featurre name.
+ * @param {string} feature Featurre name.
*
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function toggleFeature( feature ) {
return {
@@ -517,8 +517,10 @@ export const createWarningNotice = partial( createNotice, 'warning' );
* Returns an action object used to fetch a single reusable block or all
* reusable blocks from the REST API into the store.
*
- * @param {?string} id If given, only a single reusable block with this ID will be fetched
- * @return {Object} Action object
+ * @param {?string} id If given, only a single reusable block with this ID will
+ * be fetched.
+ *
+ * @returns {Object} Action object.
*/
export function fetchReusableBlocks( id ) {
return {
@@ -528,11 +530,14 @@ export function fetchReusableBlocks( id ) {
}
/**
- * Returns an action object used to insert or update a reusable block into the store.
+ * Returns an action object used to insert or update a reusable block into
+ * the store.
+ *
+ * @param {Object} id The ID of the reusable block to update.
+ * @param {Object} reusableBlock The new reusable block object. Any omitted keys
+ * are not changed.
*
- * @param {Object} id The ID of the reusable block to update
- * @param {Object} reusableBlock The new reusable block object. Any omitted keys are not changed
- * @return {Object} Action object
+ * @returns {Object} Action object.
*/
export function updateReusableBlock( id, reusableBlock ) {
return {
@@ -546,8 +551,9 @@ export function updateReusableBlock( id, reusableBlock ) {
* Returns an action object used to save a reusable block that's in the store
* to the REST API.
*
- * @param {Object} id The ID of the reusable block to save
- * @return {Object} Action object
+ * @param {Object} id The ID of the reusable block to save.
+ *
+ * @returns {Object} Action object.
*/
export function saveReusableBlock( id ) {
return {
@@ -558,9 +564,10 @@ export function saveReusableBlock( id ) {
/**
* Returns an action object used to delete a reusable block via the REST API.
- *
- * @param {number} id The ID of the reusable block to delete.
- * @return {Object} Action object.
+ *
+ * @param {number} id The ID of the reusable block to delete.
+ *
+ * @returns {Object} Action object.
*/
export function deleteReusableBlock( id ) {
return {
@@ -573,8 +580,9 @@ export function deleteReusableBlock( id ) {
* Returns an action object used to convert a reusable block into a static
* block.
*
- * @param {Object} uid The ID of the block to attach
- * @return {Object} Action object
+ * @param {Object} uid The ID of the block to attach.
+ *
+ * @returns {Object} Action object.
*/
export function convertBlockToStatic( uid ) {
return {
@@ -587,8 +595,9 @@ export function convertBlockToStatic( uid ) {
* Returns an action object used to convert a static block into a reusable
* block.
*
- * @param {Object} uid The ID of the block to detach
- * @return {Object} Action object
+ * @param {Object} uid The ID of the block to detach.
+ *
+ * @returns {Object} Action object.
*/
export function convertBlockToReusable( uid ) {
return {
diff --git a/editor/store/effects.js b/editor/store/effects.js
index 333865b956743b..c742e6bdacfb80 100644
--- a/editor/store/effects.js
+++ b/editor/store/effects.js
@@ -2,7 +2,7 @@
* External dependencies
*/
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
-import { get, includes, map, castArray, uniqueId } from 'lodash';
+import { get, includes, map, castArray, uniqueId, reduce, values, some } from 'lodash';
/**
* WordPress dependencies
@@ -36,14 +36,15 @@ import {
savePost,
editPost,
requestMetaBoxUpdates,
+ metaBoxUpdatesSuccess,
updateReusableBlock,
saveReusableBlock,
insertBlock,
+ setMetaBoxSavedData,
} from './actions';
import {
getCurrentPost,
getCurrentPostType,
- getDirtyMetaBoxes,
getEditedPostContent,
getPostEdits,
isCurrentPostPublished,
@@ -53,8 +54,10 @@ import {
getBlock,
getBlocks,
getReusableBlock,
+ getMetaBoxes,
POST_UPDATE_TRANSACTION_ID,
} from './selectors';
+import { getMetaBoxContainer } from '../edit-post/meta-boxes';
/**
* Module Constants
@@ -108,7 +111,7 @@ export default {
},
REQUEST_POST_UPDATE_SUCCESS( action, store ) {
const { previousPost, post } = action;
- const { dispatch, getState } = store;
+ const { dispatch } = store;
const publishStatus = [ 'publish', 'private', 'future' ];
const isPublished = includes( publishStatus, previousPost.status );
@@ -148,7 +151,7 @@ export default {
}
// Update dirty meta boxes.
- dispatch( requestMetaBoxUpdates( getDirtyMetaBoxes( getState() ) ) );
+ dispatch( requestMetaBoxUpdates() );
if ( get( window.history.state, 'id' ) !== post.id ) {
window.history.replaceState(
@@ -452,4 +455,42 @@ export default {
const message = spokenMessage || content;
speak( message, 'assertive' );
},
+ INITIALIZE_META_BOX_STATE( action, store ) {
+ // Allow toggling metaboxes panels
+ if ( some( action.metaBoxes ) ) {
+ window.postboxes.add_postbox_toggles( 'post' );
+ }
+ const dataPerLocation = reduce( action.metaBoxes, ( memo, isActive, location ) => {
+ if ( isActive ) {
+ memo[ location ] = jQuery( getMetaBoxContainer( location ) ).serialize();
+ }
+ return memo;
+ }, {} );
+ store.dispatch( setMetaBoxSavedData( dataPerLocation ) );
+ },
+ REQUEST_META_BOX_UPDATES( action, store ) {
+ const dataPerLocation = reduce( getMetaBoxes( store.getState() ), ( memo, metabox, location ) => {
+ if ( metabox.isActive ) {
+ memo[ location ] = jQuery( getMetaBoxContainer( location ) ).serialize();
+ }
+ return memo;
+ }, {} );
+ store.dispatch( setMetaBoxSavedData( dataPerLocation ) );
+
+ // To save the metaboxes, we serialize each one of the location forms and combine them
+ // We also add the "common" hidden fields from the base .metabox-base-form
+ const formData = values( dataPerLocation ).concat(
+ jQuery( '.metabox-base-form' ).serialize()
+ ).join( '&' );
+ const fetchOptions = {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: formData,
+ credentials: 'include',
+ };
+
+ // Save the metaboxes
+ window.fetch( window._wpMetaBoxUrl, fetchOptions )
+ .then( () => store.dispatch( metaBoxUpdatesSuccess() ) );
+ },
};
diff --git a/editor/store/index.js b/editor/store/index.js
index 4bb3318a78228b..f56edc5124ae61 100644
--- a/editor/store/index.js
+++ b/editor/store/index.js
@@ -1,27 +1,36 @@
/**
* WordPress Dependencies
*/
-import { registerReducer } from '@wordpress/data';
+import { registerReducer, registerSelectors } from '@wordpress/data';
/**
* Internal dependencies
*/
-import { PREFERENCES_DEFAULTS } from './defaults';
import reducer from './reducer';
import { withRehydratation, loadAndPersist } from './persist';
import enhanceWithBrowserSize from './mobile';
import applyMiddlewares from './middlewares';
import { BREAK_MEDIUM } from './constants';
+import {
+ getEditedPostContent,
+ getEditedPostTitle,
+} from './selectors';
/**
* Module Constants
*/
const STORAGE_KEY = `GUTENBERG_PREFERENCES_${ window.userSettings.uid }`;
+const MODULE_KEY = 'core/editor';
const store = applyMiddlewares(
registerReducer( 'core/editor', withRehydratation( reducer, 'preferences' ) )
);
-loadAndPersist( store, 'preferences', STORAGE_KEY, PREFERENCES_DEFAULTS );
+loadAndPersist( store, reducer, 'preferences', STORAGE_KEY );
enhanceWithBrowserSize( store, BREAK_MEDIUM );
+registerSelectors( MODULE_KEY, {
+ getEditedPostContent,
+ getEditedPostTitle,
+} );
+
export default store;
diff --git a/editor/store/middlewares.js b/editor/store/middlewares.js
index 9e564c23b446ea..c0a8fef307bc11 100644
--- a/editor/store/middlewares.js
+++ b/editor/store/middlewares.js
@@ -12,11 +12,11 @@ import { mobileMiddleware } from '../utils/mobile';
import effects from './effects';
/**
- * Applies the custom middlewares used specifically in the editor module
+ * Applies the custom middlewares used specifically in the editor module.
*
- * @param {Object} store Store Object
+ * @param {Object} store Store Object.
*
- * @return {Object} Update Store Object
+ * @returns {Object} Update Store Object.
*/
function applyMiddlewares( store ) {
const middlewares = [
diff --git a/editor/store/mobile.js b/editor/store/mobile.js
index 4691d37a3f3dca..797ce3a3a96dfb 100644
--- a/editor/store/mobile.js
+++ b/editor/store/mobile.js
@@ -1,8 +1,8 @@
/**
- * Enhance a redux store with the browser size
+ * Enhance a redux store with the browser size.
*
- * @param {Object} store Redux Store
- * @param {Number} mobileBreakpoint The mobile breakpoint
+ * @param {Object} store Redux Store.
+ * @param {number} mobileBreakpoint The mobile breakpoint.
*/
function enhanceWithBrowserSize( store, mobileBreakpoint ) {
const updateSize = () => {
diff --git a/editor/store/persist.js b/editor/store/persist.js
index 25ff6603d14618..b95100f5acc25d 100644
--- a/editor/store/persist.js
+++ b/editor/store/persist.js
@@ -4,12 +4,12 @@
import { get } from 'lodash';
/**
- * Adds the rehydratation behavior to redux reducers
+ * Adds the rehydratation behavior to redux reducers.
*
- * @param {Function} reducer The reducer to enhance
- * @param {String} reducerKey The reducer key to persist
+ * @param {Function} reducer The reducer to enhance.
+ * @param {string} reducerKey The reducer key to persist.
*
- * @return {Function} Enhanced reducer
+ * @returns {Function} Enhanced reducer.
*/
export function withRehydratation( reducer, reducerKey ) {
// EnhancedReducer with auto-rehydration
@@ -30,21 +30,21 @@ export function withRehydratation( reducer, reducerKey ) {
}
/**
- * Loads the initial state and persist on changes
+ * Loads the initial state and persist on changes.
*
- * This should be executed after the reducer's registration
+ * This should be executed after the reducer's registration.
*
- * @param {Object} store Store to enhance
- * @param {String} reducerKey The reducer key to persist (example: reducerKey.subReducerKey)
- * @param {String} storageKey The storage key to use
- * @param {Object} defaults Default values of the reducer key
+ * @param {Object} store Store to enhance.
+ * @param {Function} reducer The reducer function. Used to get default values and to allow custom serialization by the reducers.
+ * @param {string} reducerKey The reducer key to persist (example: reducerKey.subReducerKey).
+ * @param {string} storageKey The storage key to use.
*/
-export function loadAndPersist( store, reducerKey, storageKey, defaults = {} ) {
+export function loadAndPersist( store, reducer, reducerKey, storageKey ) {
// Load initially persisted value
const persistedString = window.localStorage.getItem( storageKey );
if ( persistedString ) {
const persistedState = {
- ...defaults,
+ ...get( reducer( undefined, { type: 'DEFAULTS' } ), reducerKey ),
...JSON.parse( persistedString ),
};
@@ -60,7 +60,8 @@ export function loadAndPersist( store, reducerKey, storageKey, defaults = {} ) {
const newStateValue = get( store.getState(), reducerKey );
if ( newStateValue !== currentStateValue ) {
currentStateValue = newStateValue;
- window.localStorage.setItem( storageKey, JSON.stringify( currentStateValue ) );
+ const stateToSave = get( reducer( store.getState(), { type: 'REDUX_SERIALIZE' } ), reducerKey );
+ window.localStorage.setItem( storageKey, JSON.stringify( stateToSave ) );
}
} );
}
diff --git a/editor/store/reducer.js b/editor/store/reducer.js
index b1f346f4e3ad6a..26d9ba77a5e783 100644
--- a/editor/store/reducer.js
+++ b/editor/store/reducer.js
@@ -42,8 +42,9 @@ const MAX_RECENT_BLOCKS = 8;
* Returns a post attribute value, flattening nested rendered content using its
* raw value in place of its original object form.
*
- * @param {*} value Original value
- * @return {*} Raw value
+ * @param {*} value Original value.
+ *
+ * @returns {*} Raw value.
*/
export function getPostRawValue( value ) {
if ( value && 'object' === typeof value && 'raw' in value ) {
@@ -63,9 +64,10 @@ export function getPostRawValue( value ) {
* - blocksByUid: post content blocks keyed by UID
* - blockOrder: list of block UIDs in order
*
- * @param {Object} state Current state
- * @param {Object} action Dispatched action
- * @return {Object} Updated state
+ * @param {Object} state Current state.
+ * @param {Object} action Dispatched action.
+ *
+ * @returns {Object} Updated state.
*/
export const editor = flow( [
combineReducers,
@@ -309,9 +311,10 @@ export const editor = flow( [
* Reducer returning the last-known state of the current post, in the format
* returned by the WP REST API.
*
- * @param {Object} state Current state
- * @param {Object} action Dispatched action
- * @return {Object} Updated state
+ * @param {Object} state Current state.
+ * @param {Object} action Dispatched action.
+ *
+ * @returns {Object} Updated state.
*/
export function currentPost( state = {}, action ) {
switch ( action.type ) {
@@ -338,9 +341,10 @@ export function currentPost( state = {}, action ) {
/**
* Reducer returning typing state.
*
- * @param {Boolean} state Current state
- * @param {Object} action Dispatched action
- * @return {Boolean} Updated state
+ * @param {boolean} state Current state.
+ * @param {Object} action Dispatched action.
+ *
+ * @returns {boolean} Updated state.
*/
export function isTyping( state = false, action ) {
switch ( action.type ) {
@@ -357,9 +361,10 @@ export function isTyping( state = false, action ) {
/**
* Reducer returning the block selection's state.
*
- * @param {Object} state Current state
- * @param {Object} action Dispatched action
- * @return {Object} Updated state
+ * @param {Object} state Current state.
+ * @param {Object} action Dispatched action.
+ *
+ * @returns {Object} Updated state.
*/
export function blockSelection( state = {
start: null,
@@ -370,6 +375,11 @@ export function blockSelection( state = {
}, action ) {
switch ( action.type ) {
case 'CLEAR_SELECTED_BLOCK':
+ if ( state.start === null && state.end === null &&
+ state.focus === null && ! state.isMultiSelecting ) {
+ return state;
+ }
+
return {
...state,
start: null,
@@ -378,15 +388,24 @@ export function blockSelection( state = {
isMultiSelecting: false,
};
case 'START_MULTI_SELECT':
+ if ( state.isMultiSelecting ) {
+ return state;
+ }
+
return {
...state,
isMultiSelecting: true,
};
case 'STOP_MULTI_SELECT':
+ const nextFocus = state.start === state.end ? state.focus : null;
+ if ( ! state.isMultiSelecting && nextFocus === state.focus ) {
+ return state;
+ }
+
return {
...state,
isMultiSelecting: false,
- focus: state.start === state.end ? state.focus : null,
+ focus: nextFocus,
};
case 'MULTI_SELECT':
return {
@@ -444,9 +463,10 @@ export function blockSelection( state = {
/**
* Reducer returning hovered block state.
*
- * @param {Object} state Current state
- * @param {Object} action Dispatched action
- * @return {Object} Updated state
+ * @param {Object} state Current state.
+ * @param {Object} action Dispatched action.
+ *
+ * @returns {Object} Updated state.
*/
export function hoveredBlock( state = null, action ) {
switch ( action.type ) {
@@ -480,11 +500,12 @@ export function blocksMode( state = {}, action ) {
}
/**
- * Reducer returning the block insertion point
+ * Reducer returning the block insertion point.
*
- * @param {Object} state Current state
- * @param {Object} action Dispatched action
- * @return {Object} Updated state
+ * @param {Object} state Current state.
+ * @param {Object} action Dispatched action.
+ *
+ * @returns {Object} Updated state.
*/
export function blockInsertionPoint( state = {}, action ) {
switch ( action.type ) {
@@ -499,14 +520,15 @@ export function blockInsertionPoint( state = {}, action ) {
}
/**
- * Reducer returning the user preferences:
+ * Reducer returning the user preferences.
+ *
+ * @param {Object} state Current state.
+ * @param {string} state.mode Current editor mode, either "visual" or "text".
+ * @param {boolean} state.isSidebarOpened Whether the sidebar is opened or closed.
+ * @param {Object} state.panels The state of the different sidebar panels.
+ * @param {Object} action Dispatched action.
*
- * @param {Object} state Current state
- * @param {string} state.mode Current editor mode, either "visual" or "text".
- * @param {Boolean} state.isSidebarOpened Whether the sidebar is opened or closed
- * @param {Object} state.panels The state of the different sidebar panels
- * @param {Object} action Dispatched action
- * @return {string} Updated state
+ * @returns {string} Updated state.
*/
export function preferences( state = PREFERENCES_DEFAULTS, action ) {
switch ( action.type ) {
@@ -570,6 +592,8 @@ export function preferences( state = PREFERENCES_DEFAULTS, action ) {
[ action.feature ]: ! state.features[ action.feature ],
},
};
+ case 'REDUX_SERIALIZE':
+ return omit( state, [ 'sidebars.mobile', 'sidebars.publish' ] );
}
return state;
@@ -585,12 +609,13 @@ export function panel( state = 'document', action ) {
}
/**
- * Reducer returning current network request state (whether a request to the WP
- * REST API is in progress, successful, or failed).
+ * Reducer returning current network request state (whether a request to
+ * the WP REST API is in progress, successful, or failed).
+ *
+ * @param {Object} state Current state.
+ * @param {Object} action Dispatched action.
*
- * @param {Object} state Current state
- * @param {Object} action Dispatched action
- * @return {Object} Updated state
+ * @returns {Object} Updated state.
*/
export function saving( state = {}, action ) {
switch ( action.type ) {
@@ -652,60 +677,61 @@ const locations = [
const defaultMetaBoxState = locations.reduce( ( result, key ) => {
result[ key ] = {
isActive: false,
- isDirty: false,
- isUpdating: false,
};
return result;
}, {} );
+/**
+ * Reducer keeping track of the meta boxes isSaving state.
+ * A "true" value means the meta boxes saving request is in-flight.
+ *
+ *
+ * @param {boolean} state Previous state.
+ * @param {Object} action Action Object.
+ * @returns {Object} Updated state.
+ */
+export function isSavingMetaBoxes( state = false, action ) {
+ switch ( action.type ) {
+ case 'REQUEST_META_BOX_UPDATES':
+ return true;
+ case 'META_BOX_UPDATES_SUCCESS':
+ return false;
+ default:
+ return state;
+ }
+}
+
+/**
+ * Reducer keeping track of the state of each meta box location.
+ * This includes:
+ * - isActive: Whether the location is active or not.
+ * - data: The last saved form data for this location.
+ * This is used to check whether the form is dirty
+ * before leaving the page.
+ *
+ * @param {boolean} state Previous state.
+ * @param {Object} action Action Object.
+ * @returns {Object} Updated state.
+ */
export function metaBoxes( state = defaultMetaBoxState, action ) {
switch ( action.type ) {
case 'INITIALIZE_META_BOX_STATE':
return locations.reduce( ( newState, location ) => {
newState[ location ] = {
...state[ location ],
- isLoaded: false,
isActive: action.metaBoxes[ location ],
};
return newState;
}, { ...state } );
- case 'META_BOX_LOADED':
- return {
- ...state,
- [ action.location ]: {
- ...state[ action.location ],
- isLoaded: true,
- isUpdating: false,
- isDirty: false,
- },
- };
- case 'HANDLE_META_BOX_RELOAD':
- return {
- ...state,
- [ action.location ]: {
- ...state[ action.location ],
- isUpdating: false,
- isDirty: false,
- },
- };
- case 'REQUEST_META_BOX_UPDATES':
- return action.locations.reduce( ( newState, location ) => {
+ case 'META_BOX_SET_SAVED_DATA':
+ return locations.reduce( ( newState, location ) => {
newState[ location ] = {
...state[ location ],
- isUpdating: true,
- isDirty: false,
+ data: action.dataPerLocation[ location ],
};
return newState;
}, { ...state } );
- case 'META_BOX_STATE_CHANGED':
- return {
- ...state,
- [ action.location ]: {
- ...state[ action.location ],
- isDirty: action.hasChanged,
- },
- };
default:
return state;
}
@@ -826,6 +852,7 @@ export default optimist( combineReducers( {
saving,
notices,
metaBoxes,
+ isSavingMetaBoxes,
mobile,
reusableBlocks,
} ) );
diff --git a/editor/store/selectors.js b/editor/store/selectors.js
index e806ef9dcc06e1..9be0d422e448ff 100644
--- a/editor/store/selectors.js
+++ b/editor/store/selectors.js
@@ -12,13 +12,14 @@ import {
without,
compact,
find,
+ some,
} from 'lodash';
import createSelector from 'rememo';
/**
* WordPress dependencies
*/
-import { serialize, getBlockType } from '@wordpress/blocks';
+import { serialize, getBlockType, getBlockTypes } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
@@ -31,8 +32,9 @@ const MAX_FREQUENT_BLOCKS = 3;
/**
* Returns the current editing mode.
*
- * @param {Object} state Global application state
- * @return {String} Editing mode
+ * @param {Object} state Global application state.
+ *
+ * @returns {string} Editing mode.
*/
export function getEditorMode( state ) {
return getPreference( state, 'mode', 'visual' );
@@ -41,8 +43,8 @@ export function getEditorMode( state ) {
/**
* Returns the state of legacy meta boxes.
*
- * @param {Object} state Global application state
- * @return {Object} State of meta boxes
+ * @param {Object} state Global application state.
+ * @returns {Object} State of meta boxes.
*/
export function getMetaBoxes( state ) {
return state.metaBoxes;
@@ -51,57 +53,57 @@ export function getMetaBoxes( state ) {
/**
* Returns the state of legacy meta boxes.
*
- * @param {Object} state Global application state
- * @param {String} location Location of the meta box.
- * @return {Object} State of meta box at specified location.
+ * @param {Object} state Global application state.
+ * @param {string} location Location of the meta box.
+ *
+ * @returns {Object} State of meta box at specified location.
*/
export function getMetaBox( state, location ) {
return getMetaBoxes( state )[ location ];
}
/**
- * Returns a list of dirty meta box locations.
+ * Returns true if the post is using Meta Boxes
*
* @param {Object} state Global application state
- * @return {Array} Array of locations for dirty meta boxes.
+ * @return {boolean} Whether there are metaboxes or not.
*/
-export const getDirtyMetaBoxes = createSelector(
+export const hasMetaBoxes = createSelector(
( state ) => {
- return reduce( getMetaBoxes( state ), ( result, metaBox, location ) => {
- return metaBox.isDirty && metaBox.isActive ?
- [ ...result, location ] :
- result;
- }, [] );
+ return some( getMetaBoxes( state ), ( metaBox ) => {
+ return metaBox.isActive;
+ } );
},
( state ) => state.metaBoxes,
);
/**
- * Returns the dirty state of legacy meta boxes.
+ * Returns true if the the Meta Boxes are being saved.
*
- * Checks whether the entire meta box state is dirty. So if a sidebar is dirty,
- * but a normal area is not dirty, this will overall return dirty.
- *
- * @param {Object} state Global application state
- * @return {Boolean} Whether state is dirty. True if dirty, false if not.
+ * @param {Object} state Global application state.
+ * @returns {boolean} Whether the metaboxes are being saved.
*/
-export const isMetaBoxStateDirty = ( state ) => getDirtyMetaBoxes( state ).length > 0;
+export function isSavingMetaBoxes( state ) {
+ return state.isSavingMetaBoxes;
+}
/**
* Returns the current active panel for the sidebar.
*
- * @param {Object} state Global application state
- * @return {String} Active sidebar panel
+ * @param {Object} state Global application state.
+ *
+ * @returns {string} Active sidebar panel.
*/
export function getActivePanel( state ) {
return state.panel;
}
/**
- * Returns the preferences (these preferences are persisted locally)
+ * Returns the preferences (these preferences are persisted locally).
+ *
+ * @param {Object} state Global application state.
*
- * @param {Object} state Global application state
- * @return {Object} Preferences Object
+ * @returns {Object} Preferences Object.
*/
export function getPreferences( state ) {
return state.preferences;
@@ -109,10 +111,11 @@ export function getPreferences( state ) {
/**
*
- * @param {Object} state Global application state
- * @param {String} preferenceKey Preference Key
- * @param {Mixed} defaultValue Default Value
- * @return {Mixed} Preference Value
+ * @param {Object} state Global application state.
+ * @param {string} preferenceKey Preference Key.
+ * @param {Mixed} defaultValue Default Value.
+ *
+ * @returns {Mixed} Preference Value.
*/
export function getPreference( state, preferenceKey, defaultValue ) {
const preferences = getPreferences( state );
@@ -123,9 +126,10 @@ export function getPreference( state, preferenceKey, defaultValue ) {
/**
* Returns true if the sidebar is open, or false otherwise.
*
- * @param {Object} state Global application state
- * @param {string} sidebar Sidebar name (leave undefined for the default sidebar)
- * @return {Boolean} Whether the given sidebar is open
+ * @param {Object} state Global application state.
+ * @param {string} sidebar Sidebar name (leave undefined for the default sidebar).
+ *
+ * @returns {boolean} Whether the given sidebar is open.
*/
export function isSidebarOpened( state, sidebar ) {
const sidebars = getPreference( state, 'sidebars' );
@@ -137,10 +141,11 @@ export function isSidebarOpened( state, sidebar ) {
}
/**
- * Returns true if there's any open sidebar (mobile, desktop or publish)
+ * Returns true if there's any open sidebar (mobile, desktop or publish).
+ *
+ * @param {Object} state Global application state.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether sidebar is open
+ * @returns {boolean} Whether sidebar is open.
*/
export function hasOpenSidebar( state ) {
const sidebars = getPreference( state, 'sidebars' );
@@ -152,9 +157,10 @@ export function hasOpenSidebar( state ) {
/**
* Returns true if the editor sidebar panel is open, or false otherwise.
*
- * @param {Object} state Global application state
- * @param {STring} panel Sidebar panel name
- * @return {Boolean} Whether sidebar is open
+ * @param {Object} state Global application state.
+ * @param {string} panel Sidebar panel name.
+ *
+ * @returns {boolean} Whether sidebar is open.
*/
export function isEditorSidebarPanelOpened( state, panel ) {
const panels = getPreference( state, 'panels' );
@@ -164,8 +170,9 @@ export function isEditorSidebarPanelOpened( state, panel ) {
/**
* Returns true if any past editor history snapshots exist, or false otherwise.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether undo history exists
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether undo history exists.
*/
export function hasEditorUndo( state ) {
return state.editor.past.length > 0;
@@ -175,8 +182,9 @@ export function hasEditorUndo( state ) {
* Returns true if any future editor history snapshots exist, or false
* otherwise.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether redo history exists
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether redo history exists.
*/
export function hasEditorRedo( state ) {
return state.editor.future.length > 0;
@@ -186,8 +194,9 @@ export function hasEditorRedo( state ) {
* Returns true if the currently edited post is yet to be saved, or false if
* the post has been saved.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether the post is new
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether the post is new.
*/
export function isEditedPostNew( state ) {
return getCurrentPost( state ).status === 'auto-draft';
@@ -197,29 +206,33 @@ export function isEditedPostNew( state ) {
* Returns true if there are unsaved values for the current edit session, or
* false if the editing state matches the saved or new post.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether unsaved values exist
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether unsaved values exist.
*/
export function isEditedPostDirty( state ) {
- return state.editor.isDirty || isMetaBoxStateDirty( state );
+ return state.editor.isDirty;
}
/**
* Returns true if there are no unsaved values for the current edit session and if
* the currently edited post is new (and has never been saved before).
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether new post and unsaved values exist
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether new post and unsaved values exist.
*/
export function isCleanNewPost( state ) {
return ! isEditedPostDirty( state ) && isEditedPostNew( state );
}
/**
- * Returns true if the current window size corresponds to mobile resolutions (<= medium breakpoint)
+ * Returns true if the current window size corresponds to mobile resolutions (<= medium breakpoint).
+ *
+ * @param {Object} state Global application state.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether current window size corresponds to mobile resolutions
+ * @returns {boolean} Whether current window size corresponds to
+ * mobile resolutions.
*/
export function isMobile( state ) {
return state.mobile;
@@ -230,18 +243,20 @@ export function isMobile( state ) {
* including unsaved edits. Returns an object containing relevant default post
* values if the post has not yet been saved.
*
- * @param {Object} state Global application state
- * @return {Object} Post object
+ * @param {Object} state Global application state.
+ *
+ * @returns {Object} Post object.
*/
export function getCurrentPost( state ) {
return state.currentPost;
}
/**
- * Returns the post type of the post currently being edited
+ * Returns the post type of the post currently being edited.
*
- * @param {Object} state Global application state
- * @return {String} Post type
+ * @param {Object} state Global application state.
+ *
+ * @returns {string} Post type.
*/
export function getCurrentPostType( state ) {
return state.currentPost.type;
@@ -251,8 +266,9 @@ export function getCurrentPostType( state ) {
* Returns the ID of the post currently being edited, or null if the post has
* not yet been saved.
*
- * @param {Object} state Global application state
- * @return {?Number} ID of current post
+ * @param {Object} state Global application state.
+ *
+ * @returns {?Number} ID of current post.
*/
export function getCurrentPostId( state ) {
return getCurrentPost( state ).id || null;
@@ -261,8 +277,9 @@ export function getCurrentPostId( state ) {
/**
* Returns the number of revisions of the post currently being edited.
*
- * @param {Object} state Global application state
- * @return {Number} Number of revisions
+ * @param {Object} state Global application state.
+ *
+ * @returns {number} Number of revisions.
*/
export function getCurrentPostRevisionsCount( state ) {
return get( getCurrentPost( state ), 'revisions.count', 0 );
@@ -272,8 +289,9 @@ export function getCurrentPostRevisionsCount( state ) {
* Returns the last revision ID of the post currently being edited,
* or null if the post has no revisions.
*
- * @param {Object} state Global application state
- * @return {?Number} ID of the last revision
+ * @param {Object} state Global application state.
+ *
+ * @returns {?Number} ID of the last revision.
*/
export function getCurrentPostLastRevisionId( state ) {
return get( getCurrentPost( state ), 'revisions.last_id', null );
@@ -283,8 +301,9 @@ export function getCurrentPostLastRevisionId( state ) {
* Returns any post values which have been changed in the editor but not yet
* been saved.
*
- * @param {Object} state Global application state
- * @return {Object} Object of key value pairs comprising unsaved edits
+ * @param {Object} state Global application state.
+ *
+ * @returns {Object} Object of key value pairs comprising unsaved edits.
*/
export function getPostEdits( state ) {
return state.editor.present.edits;
@@ -295,9 +314,10 @@ export function getPostEdits( state ) {
* edit if one exists, but falling back to the attribute for the last known
* saved state of the post.
*
- * @param {Object} state Global application state
- * @param {String} attributeName Post attribute name
- * @return {*} Post attribute value
+ * @param {Object} state Global application state.
+ * @param {string} attributeName Post attribute name.
+ *
+ * @returns {*} Post attribute value.
*/
export function getEditedPostAttribute( state, attributeName ) {
return state.editor.present.edits[ attributeName ] === undefined ?
@@ -310,8 +330,9 @@ export function getEditedPostAttribute( state, attributeName ) {
* unsaved value if different than the saved post. The return value is one of
* "private", "password", or "public".
*
- * @param {Object} state Global application state
- * @return {String} Post visibility
+ * @param {Object} state Global application state.
+ *
+ * @returns {string} Post visibility.
*/
export function getEditedPostVisibility( state ) {
const status = getEditedPostAttribute( state, 'status' );
@@ -328,8 +349,9 @@ export function getEditedPostVisibility( state ) {
/**
* Return true if the current post has already been published.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether the post has been published
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether the post has been published.
*/
export function isCurrentPostPublished( state ) {
const post = getCurrentPost( state );
@@ -339,22 +361,24 @@ export function isCurrentPostPublished( state ) {
}
/**
- * Return true if the post being edited can be published
+ * Return true if the post being edited can be published.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether the post can been published
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether the post can been published.
*/
export function isEditedPostPublishable( state ) {
const post = getCurrentPost( state );
- return isEditedPostDirty( state ) || [ 'publish', 'private', 'future' ].indexOf( post.status ) === -1;
+ return isEditedPostDirty( state ) || hasMetaBoxes( state ) || [ 'publish', 'private', 'future' ].indexOf( post.status ) === -1;
}
/**
* Returns true if the post can be saved, or false otherwise. A post must
* contain a title, an excerpt, or non-empty content to be valid for save.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether the post can be saved
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether the post can be saved.
*/
export function isEditedPostSaveable( state ) {
return (
@@ -368,8 +392,9 @@ export function isEditedPostSaveable( state ) {
* Return true if the post being edited is being scheduled. Preferring the
* unsaved status values.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether the post has been published
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether the post has been published.
*/
export function isEditedPostBeingScheduled( state ) {
const date = getEditedPostAttribute( state, 'date' );
@@ -383,8 +408,9 @@ export function isEditedPostBeingScheduled( state ) {
* Returns the raw title of the post being edited, preferring the unsaved value
* if different than the saved post.
*
- * @param {Object} state Global application state
- * @return {String} Raw post title
+ * @param {Object} state Global application state.
+ *
+ * @returns {string} Raw post title.
*/
export function getEditedPostTitle( state ) {
const editedTitle = getPostEdits( state ).title;
@@ -401,8 +427,9 @@ export function getEditedPostTitle( state ) {
/**
* Gets the document title to be used.
*
- * @param {Object} state Global application state
- * @return {string} Document title
+ * @param {Object} state Global application state.
+ *
+ * @returns {string} Document title.
*/
export function getDocumentTitle( state ) {
let title = getEditedPostTitle( state );
@@ -417,8 +444,9 @@ export function getDocumentTitle( state ) {
* Returns the raw excerpt of the post being edited, preferring the unsaved
* value if different than the saved post.
*
- * @param {Object} state Global application state
- * @return {String} Raw post excerpt
+ * @param {Object} state Global application state.
+ *
+ * @returns {string} Raw post excerpt.
*/
export function getEditedPostExcerpt( state ) {
return state.editor.present.edits.excerpt === undefined ?
@@ -429,8 +457,9 @@ export function getEditedPostExcerpt( state ) {
/**
* Returns a URL to preview the post being edited.
*
- * @param {Object} state Global application state
- * @return {String} Preview URL
+ * @param {Object} state Global application state.
+ *
+ * @returns {string} Preview URL.
*/
export function getEditedPostPreviewLink( state ) {
const link = state.currentPost.link;
@@ -447,9 +476,10 @@ export function getEditedPostPreviewLink( state ) {
* state. This is not the block's registration settings, which must be
* retrieved from the blocks module registration store.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Object} Parsed block object
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {Object} Parsed block object.
*/
export const getBlock = createSelector(
( state, uid ) => {
@@ -501,8 +531,9 @@ function getPostMeta( state, key ) {
* the order they appear in the post.
* Note: It's important to memoize this selector to avoid return a new instance on each call
*
- * @param {Object} state Global application state
- * @return {Object[]} Post blocks
+ * @param {Object} state Global application state.
+ *
+ * @returns {Object[]} Post blocks.
*/
export const getBlocks = createSelector(
( state ) => {
@@ -517,8 +548,9 @@ export const getBlocks = createSelector(
/**
* Returns the number of blocks currently present in the post.
*
- * @param {Object} state Global application state
- * @return {Number} Number of blocks in the post
+ * @param {Object} state Global application state.
+ *
+ * @returns {number} Number of blocks in the post.
*/
export function getBlockCount( state ) {
return getBlockUids( state ).length;
@@ -527,8 +559,9 @@ export function getBlockCount( state ) {
/**
* Returns the number of blocks currently selected in the post.
*
- * @param {Object} state Global application state
- * @return {Number} Number of blocks selected in the post
+ * @param {Object} state Global application state.
+ *
+ * @returns {number} Number of blocks selected in the post.
*/
export function getSelectedBlockCount( state ) {
const multiSelectedBlockCount = getMultiSelectedBlockUids( state ).length;
@@ -543,8 +576,9 @@ export function getSelectedBlockCount( state ) {
/**
* Returns the currently selected block, or null if there is no selected block.
*
- * @param {Object} state Global application state
- * @return {?Object} Selected block
+ * @param {Object} state Global application state.
+ *
+ * @returns {?Object} Selected block.
*/
export function getSelectedBlock( state ) {
const { start, end } = state.blockSelection;
@@ -559,8 +593,9 @@ export function getSelectedBlock( state ) {
* Returns the current multi-selection set of blocks unique IDs, or an empty
* array if there is no multi-selection.
*
- * @param {Object} state Global application state
- * @return {Array} Multi-selected block unique UDs
+ * @param {Object} state Global application state.
+ *
+ * @returns {Array} Multi-selected block unique IDs.
*/
export const getMultiSelectedBlockUids = createSelector(
( state ) => {
@@ -590,8 +625,9 @@ export const getMultiSelectedBlockUids = createSelector(
* Returns the current multi-selection set of blocks, or an empty array if
* there is no multi-selection.
*
- * @param {Object} state Global application state
- * @return {Array} Multi-selected block objects
+ * @param {Object} state Global application state.
+ *
+ * @returns {Array} Multi-selected block objects.
*/
export const getMultiSelectedBlocks = createSelector(
( state ) => getMultiSelectedBlockUids( state ).map( ( uid ) => getBlock( state, uid ) ),
@@ -609,8 +645,9 @@ export const getMultiSelectedBlocks = createSelector(
* Returns the unique ID of the first block in the multi-selection set, or null
* if there is no multi-selection.
*
- * @param {Object} state Global application state
- * @return {?String} First unique block ID in the multi-selection set
+ * @param {Object} state Global application state.
+ *
+ * @returns {?String} First unique block ID in the multi-selection set.
*/
export function getFirstMultiSelectedBlockUid( state ) {
return first( getMultiSelectedBlockUids( state ) ) || null;
@@ -620,8 +657,9 @@ export function getFirstMultiSelectedBlockUid( state ) {
* Returns the unique ID of the last block in the multi-selection set, or null
* if there is no multi-selection.
*
- * @param {Object} state Global application state
- * @return {?String} Last unique block ID in the multi-selection set
+ * @param {Object} state Global application state.
+ *
+ * @returns {?String} Last unique block ID in the multi-selection set.
*/
export function getLastMultiSelectedBlockUid( state ) {
return last( getMultiSelectedBlockUids( state ) ) || null;
@@ -632,9 +670,10 @@ export function getLastMultiSelectedBlockUid( state ) {
* specified unique ID is the first block of the multi-selection set, or false
* otherwise.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Boolean} Whether block is first in mult-selection
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {boolean} Whether block is first in mult-selection.
*/
export function isFirstMultiSelectedBlock( state, uid ) {
return getFirstMultiSelectedBlockUid( state ) === uid;
@@ -644,9 +683,10 @@ export function isFirstMultiSelectedBlock( state, uid ) {
* Returns true if the unique ID occurs within the block multi-selection, or
* false otherwise.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Boolean} Whether block is in multi-selection set
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {boolean} Whether block is in multi-selection set.
*/
export function isBlockMultiSelected( state, uid ) {
return getMultiSelectedBlockUids( state ).indexOf( uid ) !== -1;
@@ -659,8 +699,9 @@ export function isBlockMultiSelected( state, uid ) {
* N.b.: This is not necessarily the first uid in the selection. See
* getFirstMultiSelectedBlockUid().
*
- * @param {Object} state Global application state
- * @return {?String} Unique ID of block beginning multi-selection
+ * @param {Object} state Global application state.
+ *
+ * @returns {?String} Unique ID of block beginning multi-selection.
*/
export function getMultiSelectedBlocksStartUid( state ) {
const { start, end } = state.blockSelection;
@@ -677,8 +718,9 @@ export function getMultiSelectedBlocksStartUid( state ) {
* N.b.: This is not necessarily the last uid in the selection. See
* getLastMultiSelectedBlockUid().
*
- * @param {Object} state Global application state
- * @return {?String} Unique ID of block ending multi-selection
+ * @param {Object} state Global application state.
+ *
+ * @returns {?String} Unique ID of block ending multi-selection.
*/
export function getMultiSelectedBlocksEndUid( state ) {
const { start, end } = state.blockSelection;
@@ -692,20 +734,22 @@ export function getMultiSelectedBlocksEndUid( state ) {
* Returns an array containing all block unique IDs of the post being edited,
* in the order they appear in the post.
*
- * @param {Object} state Global application state
- * @return {Array} Ordered unique IDs of post blocks
+ * @param {Object} state Global application state.
+ *
+ * @returns {Array} Ordered unique IDs of post blocks.
*/
export function getBlockUids( state ) {
return state.editor.present.blockOrder;
}
/**
- * Returns the index at which the block corresponding to the specified unique
- * ID occurs within the post block order, or `-1` if the block does not exist.
+ * Returns the index at which the block corresponding to the specified unique ID
+ * occurs within the post block order, or `-1` if the block does not exist.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Number} Index at which block exists in order
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {number} Index at which block exists in order.
*/
export function getBlockIndex( state, uid ) {
return state.editor.present.blockOrder.indexOf( uid );
@@ -715,9 +759,10 @@ export function getBlockIndex( state, uid ) {
* Returns true if the block corresponding to the specified unique ID is the
* first block of the post, or false otherwise.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Boolean} Whether block is first in post
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {boolean} Whether block is first in post.
*/
export function isFirstBlock( state, uid ) {
return first( state.editor.present.blockOrder ) === uid;
@@ -727,9 +772,10 @@ export function isFirstBlock( state, uid ) {
* Returns true if the block corresponding to the specified unique ID is the
* last block of the post, or false otherwise.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Boolean} Whether block is last in post
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {boolean} Whether block is last in post.
*/
export function isLastBlock( state, uid ) {
return last( state.editor.present.blockOrder ) === uid;
@@ -739,9 +785,10 @@ export function isLastBlock( state, uid ) {
* Returns the block object occurring before the one corresponding to the
* specified unique ID.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Object} Block occurring before specified unique ID
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {Object} Block occurring before specified unique ID.
*/
export function getPreviousBlock( state, uid ) {
const order = getBlockIndex( state, uid );
@@ -752,9 +799,10 @@ export function getPreviousBlock( state, uid ) {
* Returns the block object occurring after the one corresponding to the
* specified unique ID.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Object} Block occurring after specified unique ID
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {Object} Block occurring after specified unique ID.
*/
export function getNextBlock( state, uid ) {
const order = getBlockIndex( state, uid );
@@ -765,9 +813,10 @@ export function getNextBlock( state, uid ) {
* Returns true if the block corresponding to the specified unique ID is
* currently selected and no multi-selection exists, or false otherwise.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Boolean} Whether block is selected and multi-selection exists
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {boolean} Whether block is selected and multi-selection exists.
*/
export function isBlockSelected( state, uid ) {
const { start, end } = state.blockSelection;
@@ -785,9 +834,11 @@ export function isBlockSelected( state, uid ) {
* refers to the block sequence in the document, _not_ the sequence of
* multi-selection, which is why `state.blockSelection.end` isn't used.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Boolean} Whether block is selected and not the last in the selection
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {boolean} Whether block is selected and not the last in
+ * the selection.
*/
export function isBlockWithinSelection( state, uid ) {
if ( ! uid ) {
@@ -803,9 +854,10 @@ export function isBlockWithinSelection( state, uid ) {
* Returns true if the cursor is hovering the block corresponding to the
* specified unique ID, or false otherwise.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Boolean} Whether block is hovered
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {boolean} Whether block is hovered.
*/
export function isBlockHovered( state, uid ) {
return state.hoveredBlock === uid;
@@ -816,9 +868,10 @@ export function isBlockHovered( state, uid ) {
* or null if the block is not selected. It is left to a block's implementation
* to manage the content of this object, defaulting to an empty object.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Object} Block focus state
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {Object} Block focus state.
*/
export function getBlockFocus( state, uid ) {
// If there is multi-selection, keep returning the focus object for the start block.
@@ -832,8 +885,9 @@ export function getBlockFocus( state, uid ) {
/**
* Whether in the process of multi-selecting or not.
*
- * @param {Object} state Global application state
- * @return {Boolean} True if multi-selecting, false if not.
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} True if multi-selecting, false if not.
*/
export function isMultiSelecting( state ) {
return state.blockSelection.isMultiSelecting;
@@ -842,19 +896,21 @@ export function isMultiSelecting( state ) {
/**
* Whether is selection disable or not.
*
- * @param {Object} state Global application state
- * @return {Boolean} True if multi is disable, false if not.
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} True if multi is disable, false if not.
*/
export function isSelectionEnabled( state ) {
return state.blockSelection.isEnabled;
}
/**
- * Returns thee block's editing mode
+ * Returns thee block's editing mode.
*
- * @param {Object} state Global application state
- * @param {String} uid Block unique ID
- * @return {Object} Block editing mode
+ * @param {Object} state Global application state.
+ * @param {string} uid Block unique ID.
+ *
+ * @returns {Object} Block editing mode.
*/
export function getBlockMode( state, uid ) {
return state.blocksMode[ uid ] || 'visual';
@@ -863,8 +919,9 @@ export function getBlockMode( state, uid ) {
/**
* Returns true if the user is typing, or false otherwise.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether user is typing
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether user is typing.
*/
export function isTyping( state ) {
return state.isTyping;
@@ -872,10 +929,11 @@ export function isTyping( state ) {
/**
* Returns the insertion point, the index at which the new inserted block would
- * be placed. Defaults to the last position
+ * be placed. Defaults to the last position.
+ *
+ * @param {Object} state Global application state.
*
- * @param {Object} state Global application state
- * @return {?String} Unique ID after which insertion will occur
+ * @returns {?String} Unique ID after which insertion will occur.
*/
export function getBlockInsertionPoint( state ) {
if ( getEditorMode( state ) !== 'visual' ) {
@@ -904,8 +962,9 @@ export function getBlockInsertionPoint( state ) {
* Returns the position at which the block inserter will insert a new adjacent
* sibling block, or null if the inserter is not actively visible.
*
- * @param {Object} state Global application state
- * @return {?Number} Whether the inserter is currently visible
+ * @param {Object} state Global application state.
+ *
+ * @returns {?Number} Whether the inserter is currently visible.
*/
export function getBlockSiblingInserterPosition( state ) {
const { position } = state.blockInsertionPoint;
@@ -917,10 +976,11 @@ export function getBlockSiblingInserterPosition( state ) {
}
/**
- * Returns true if we should show the block insertion point
+ * Returns true if we should show the block insertion point.
+ *
+ * @param {Object} state Global application state.
*
- * @param {Object} state Global application state
- * @return {?Boolean} Whether the insertion point is visible or not
+ * @returns {?Boolean} Whether the insertion point is visible or not.
*/
export function isBlockInsertionPointVisible( state ) {
return !! state.blockInsertionPoint.visible;
@@ -929,19 +989,21 @@ export function isBlockInsertionPointVisible( state ) {
/**
* Returns true if the post is currently being saved, or false otherwise.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether post is being saved
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether post is being saved.
*/
export function isSavingPost( state ) {
- return state.saving.requesting;
+ return state.saving.requesting || isSavingMetaBoxes( state );
}
/**
* Returns true if a previous post save was attempted successfully, or false
* otherwise.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether the post was saved successfully
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether the post was saved successfully.
*/
export function didPostSaveRequestSucceed( state ) {
return state.saving.successful;
@@ -951,8 +1013,9 @@ export function didPostSaveRequestSucceed( state ) {
* Returns true if a previous post save was attempted but failed, or false
* otherwise.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether the post save failed
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} Whether the post save failed.
*/
export function didPostSaveRequestFail( state ) {
return !! state.saving.error;
@@ -963,8 +1026,9 @@ export function didPostSaveRequestFail( state ) {
* is a single block within the post and it is of a type known to match a
* default post format. Returns null if the format cannot be determined.
*
- * @param {Object} state Global application state
- * @return {?String} Suggested post format
+ * @param {Object} state Global application state.
+ *
+ * @returns {?String} Suggested post format.
*/
export function getSuggestedPostFormat( state ) {
const blocks = state.editor.present.blockOrder;
@@ -1010,8 +1074,9 @@ export function getSuggestedPostFormat( state ) {
* 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
+ * @param {Object} state Global application state.
+ *
+ * @returns {string} Post content.
*/
export const getEditedPostContent = createSelector(
( state ) => {
@@ -1030,24 +1095,147 @@ export const getEditedPostContent = createSelector(
);
/**
- * Returns the user notices array
+ * Returns the user notices array.
+ *
+ * @param {Object} state Global application state.
*
- * @param {Object} state Global application state
- * @return {Array} List of notices
+ * @returns {Array} List of notices.
*/
export function getNotices( state ) {
return state.notices;
}
/**
- * Resolves the list of recently used block names into a list of block type settings.
+ * An item that appears in the inserter. Inserting this item will create a new
+ * block. Inserter items encapsulate both regular blocks and reusable blocks.
*
- * @param {Object} state Global application state
- * @return {Array} List of recently used blocks
+ * @typedef {Object} Editor.InserterItem
+ * @property {string} id Unique identifier for the item.
+ * @property {string} name The type of block to create.
+ * @property {Object} initialAttributes Attributes to pass to the newly created block.
+ * @property {string} title Title of the item, as it appears in the inserter.
+ * @property {string} icon Dashicon for the item, as it appears in the inserter.
+ * @property {string} category Block category that the item is associated with.
+ * @property {string[]} keywords Keywords that can be searched to find this item.
+ * @property {boolean} isDisabled Whether or not the user should be prevented from inserting this item.
*/
-export function getRecentlyUsedBlocks( state ) {
- // resolves the block names in the state to the block type settings
- return compact( state.preferences.recentlyUsedBlocks.map( blockType => getBlockType( blockType ) ) );
+
+/**
+ * Given a regular block type, constructs an item that appears in the inserter.
+ *
+ * @param {Object} state Global application state.
+ * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types.
+ * @param {Object} blockType Block type, likely from getBlockType().
+ *
+ * @returns {Editor.InserterItem} Item that appears in inserter.
+ */
+function buildInserterItemFromBlockType( state, enabledBlockTypes, blockType ) {
+ if ( ! enabledBlockTypes || ! blockType ) {
+ return null;
+ }
+
+ const blockTypeIsDisabled = Array.isArray( enabledBlockTypes ) && ! enabledBlockTypes.includes( blockType.name );
+ if ( blockTypeIsDisabled ) {
+ return null;
+ }
+
+ if ( blockType.isPrivate ) {
+ return null;
+ }
+
+ return {
+ id: blockType.name,
+ name: blockType.name,
+ initialAttributes: {},
+ title: blockType.title,
+ icon: blockType.icon,
+ category: blockType.category,
+ keywords: blockType.keywords,
+ isDisabled: !! blockType.useOnce && getBlocks( state ).some( block => block.name === blockType.name ),
+ };
+}
+
+/**
+ * Given a reusable block, constructs an item that appears in the inserter.
+ *
+ * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types.
+ * @param {Object} reusableBlock Reusable block, likely from getReusableBlock().
+ *
+ * @returns {Editor.InserterItem} Item that appears in inserter.
+ */
+function buildInserterItemFromReusableBlock( enabledBlockTypes, reusableBlock ) {
+ if ( ! enabledBlockTypes || ! reusableBlock ) {
+ return null;
+ }
+
+ const blockTypeIsDisabled = Array.isArray( enabledBlockTypes ) && ! enabledBlockTypes.includes( 'core/block' );
+ if ( blockTypeIsDisabled ) {
+ return null;
+ }
+
+ const referencedBlockType = getBlockType( reusableBlock.type );
+ if ( ! referencedBlockType ) {
+ return null;
+ }
+
+ return {
+ id: `core/block/${ reusableBlock.id }`,
+ name: 'core/block',
+ initialAttributes: { ref: reusableBlock.id },
+ title: reusableBlock.title,
+ icon: referencedBlockType.icon,
+ category: 'reusable-blocks',
+ keywords: [],
+ isDisabled: false,
+ };
+}
+
+/**
+ * Determines the items that appear in the the inserter. Includes both static
+ * items (e.g. a regular block type) and dynamic items (e.g. a reusable block).
+ *
+ * @param {Object} state Global application state.
+ * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types.
+ *
+ * @returns {Editor.InserterItem[]} Items that appear in inserter.
+ */
+export function getInserterItems( state, enabledBlockTypes = true ) {
+ if ( ! enabledBlockTypes ) {
+ return [];
+ }
+
+ const staticItems = getBlockTypes().map( blockType =>
+ buildInserterItemFromBlockType( state, enabledBlockTypes, blockType )
+ );
+
+ const dynamicItems = getReusableBlocks( state ).map( reusableBlock =>
+ buildInserterItemFromReusableBlock( enabledBlockTypes, reusableBlock )
+ );
+
+ const items = [ ...staticItems, ...dynamicItems ];
+ return compact( items );
+}
+
+/**
+ * Determines the items that appear in the 'Recent' tab of the inserter.
+ *
+ * @param {Object} state Global application state.
+ * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types.
+ *
+ * @returns {Editor.InserterItem[]} Items that appear in the 'Recent' tab.
+ */
+export function getRecentInserterItems( state, enabledBlockTypes = true ) {
+ if ( ! enabledBlockTypes ) {
+ return [];
+ }
+
+ const items = state.preferences.recentlyUsedBlocks.map( name =>
+ buildInserterItemFromBlockType( state, enabledBlockTypes, getBlockType( name ) )
+ );
+
+ // TODO: Merge in recently used reusable blocks
+
+ return compact( items );
}
/**
@@ -1055,8 +1243,9 @@ export function getRecentlyUsedBlocks( state ) {
* Memoized so we're not generating block lists every time we render the list
* in the inserter.
*
- * @param {Object} state Global application state
- * @return {Array} List of block type settings
+ * @param {Object} state Global application state.
+ *
+ * @returns {Array} List of block type settings.
*/
export const getMostFrequentlyUsedBlocks = createSelector(
( state ) => {
@@ -1074,19 +1263,21 @@ export const getMostFrequentlyUsedBlocks = createSelector(
/**
* Returns whether the toolbar should be fixed or not.
*
- * @param {Object} state Global application state.
- * @return {Boolean} True if toolbar is fixed.
+ * @param {Object} state Global application state.
+ *
+ * @returns {boolean} True if toolbar is fixed.
*/
export function hasFixedToolbar( state ) {
return ! isMobile( state ) && isFeatureActive( state, 'fixedToolbar' );
}
/**
- * Returns whether the given feature is enabled or not
+ * Returns whether the given feature is enabled or not.
*
- * @param {Object} state Global application state
- * @param {String} feature Feature slug
- * @return {Booleean} Is active
+ * @param {Object} state Global application state.
+ * @param {string} feature Feature slug.
+ *
+ * @returns {booleean} Is active.
*/
export function isFeatureActive( state, feature ) {
return !! state.preferences.features[ feature ];
@@ -1095,9 +1286,10 @@ export function isFeatureActive( state, feature ) {
/**
* Returns the reusable block with the given ID.
*
- * @param {Object} state Global application state
- * @param {String} ref The reusable block's ID
- * @return {Object} The reusable block, or null if none exists
+ * @param {Object} state Global application state.
+ * @param {string} ref The reusable block's ID.
+ *
+ * @returns {Object} The reusable block, or null if none exists.
*/
export function getReusableBlock( state, ref ) {
return state.reusableBlocks.data[ ref ] || null;
@@ -1106,9 +1298,10 @@ export function getReusableBlock( state, ref ) {
/**
* Returns whether or not the reusable block with the given ID is being saved.
*
- * @param {*} state Global application state
- * @param {*} ref The reusable block's ID
- * @return {Boolean} Whether or not the reusable block is being saved
+ * @param {*} state Global application state.
+ * @param {*} ref The reusable block's ID.
+ *
+ * @returns {boolean} Whether or not the reusable block is being saved.
*/
export function isSavingReusableBlock( state, ref ) {
return state.reusableBlocks.isSaving[ ref ] || false;
@@ -1117,8 +1310,9 @@ export function isSavingReusableBlock( state, ref ) {
/**
* Returns an array of all reusable blocks.
*
- * @param {Object} state Global application state
- * @return {Array} An array of all reusable blocks.
+ * @param {Object} state Global application state.
+ *
+ * @returns {Array} An array of all reusable blocks.
*/
export function getReusableBlocks( state ) {
return Object.values( state.reusableBlocks.data );
@@ -1128,9 +1322,10 @@ export function getReusableBlocks( state ) {
* Returns state object prior to a specified optimist transaction ID, or `null`
* if the transaction corresponding to the given ID cannot be found.
*
- * @param {Object} state Current global application state
- * @param {Object} transactionId Optimist transaction ID
- * @return {Object} Global application state prior to transaction
+ * @param {Object} state Current global application state.
+ * @param {Object} transactionId Optimist transaction ID.
+ *
+ * @returns {Object} Global application state prior to transaction.
*/
export function getStateBeforeOptimisticTransaction( state, transactionId ) {
const transaction = find( state.optimist, ( entry ) => (
@@ -1142,10 +1337,11 @@ export function getStateBeforeOptimisticTransaction( state, transactionId ) {
}
/**
- * Returns true if the post is being published, or false otherwise
+ * Returns true if the post is being published, or false otherwise.
+ *
+ * @param {Object} state Global application state.
*
- * @param {Object} state Global application state
- * @return {Boolean} Whether post is being published
+ * @returns {boolean} Whether post is being published.
*/
export function isPublishingPost( state ) {
if ( ! isSavingPost( state ) ) {
diff --git a/editor/store/test/actions.js b/editor/store/test/actions.js
index cd4ad7fb01bd6f..f97c7abfe5f512 100644
--- a/editor/store/test/actions.js
+++ b/editor/store/test/actions.js
@@ -7,8 +7,6 @@ import {
startTyping,
stopTyping,
requestMetaBoxUpdates,
- handleMetaBoxReload,
- metaBoxStateChanged,
initializeMetaBoxState,
fetchReusableBlocks,
updateReusableBlock,
@@ -52,7 +50,6 @@ import {
createErrorNotice,
createWarningNotice,
removeNotice,
- metaBoxLoaded,
toggleFeature,
} from '../actions';
@@ -515,16 +512,6 @@ describe( 'actions', () => {
} );
} );
- describe( 'metaBoxLoaded', () => {
- it( 'should return META_BOX_LOADED action', () => {
- const location = 'normal';
- expect( metaBoxLoaded( location ) ).toEqual( {
- type: 'META_BOX_LOADED',
- location,
- } );
- } );
- } );
-
describe( 'toggleFeature', () => {
it( 'should return TOGGLE_FEATURE action', () => {
const feature = 'name';
@@ -537,28 +524,8 @@ describe( 'actions', () => {
describe( 'requestMetaBoxUpdates', () => {
it( 'should return the REQUEST_META_BOX_UPDATES action', () => {
- expect( requestMetaBoxUpdates( [ 'normal' ] ) ).toEqual( {
+ expect( requestMetaBoxUpdates() ).toEqual( {
type: 'REQUEST_META_BOX_UPDATES',
- locations: [ 'normal' ],
- } );
- } );
- } );
-
- describe( 'handleMetaBoxReload', () => {
- it( 'should return the HANDLE_META_BOX_RELOAD action with a location and node', () => {
- expect( handleMetaBoxReload( 'normal' ) ).toEqual( {
- type: 'HANDLE_META_BOX_RELOAD',
- location: 'normal',
- } );
- } );
- } );
-
- describe( 'metaBoxStateChanged', () => {
- it( 'should return the META_BOX_STATE_CHANGED action with a hasChanged flag', () => {
- expect( metaBoxStateChanged( 'normal', true ) ).toEqual( {
- type: 'META_BOX_STATE_CHANGED',
- location: 'normal',
- hasChanged: true,
} );
} );
} );
diff --git a/editor/store/test/effects.js b/editor/store/test/effects.js
index 5d5a27cd52f9d6..4e221af79f4b14 100644
--- a/editor/store/test/effects.js
+++ b/editor/store/test/effects.js
@@ -283,7 +283,7 @@ describe( 'effects', () => {
describe( '.REQUEST_POST_UPDATE_SUCCESS', () => {
const handler = effects.REQUEST_POST_UPDATE_SUCCESS;
- let getDirtyMetaBoxesSpy, replaceStateSpy;
+ let replaceStateSpy;
const defaultPost = {
id: 1,
@@ -304,18 +304,14 @@ describe( 'effects', () => {
} );
beforeAll( () => {
- getDirtyMetaBoxesSpy = jest.spyOn( selectors, 'getDirtyMetaBoxes' );
replaceStateSpy = jest.spyOn( window.history, 'replaceState' );
} );
beforeEach( () => {
- getDirtyMetaBoxesSpy.mockReset();
- getDirtyMetaBoxesSpy.mockReturnValue( [ 'normal', 'side' ] );
replaceStateSpy.mockReset();
} );
afterAll( () => {
- getDirtyMetaBoxesSpy.mockRestore();
replaceStateSpy.mockRestore();
} );
@@ -328,7 +324,7 @@ describe( 'effects', () => {
handler( { post: post, previousPost: post }, store );
expect( dispatch ).toHaveBeenCalledTimes( 1 );
- expect( dispatch ).toHaveBeenCalledWith( requestMetaBoxUpdates( [ 'normal', 'side' ] ) );
+ expect( dispatch ).toHaveBeenCalledWith( requestMetaBoxUpdates() );
} );
it( 'should dispatch notices when publishing or scheduling a post', () => {
diff --git a/editor/store/test/persist.js b/editor/store/test/persist.js
index 0cf2df49d4f5c4..5078e807f6175d 100644
--- a/editor/store/test/persist.js
+++ b/editor/store/test/persist.js
@@ -9,7 +9,7 @@ import { createStore } from 'redux';
import { loadAndPersist, withRehydratation } from '../persist';
describe( 'loadAndPersist', () => {
- it( 'should load the initial value from the local storage', () => {
+ it( 'should load the initial value from the local storage integrating it into reducer default value.', () => {
const storageKey = 'dumbStorageKey';
window.localStorage.setItem( storageKey, JSON.stringify( { chicken: true } ) );
const reducer = () => {
@@ -20,15 +20,20 @@ describe( 'loadAndPersist', () => {
const store = createStore( withRehydratation( reducer, 'preferences' ) );
loadAndPersist(
store,
+ reducer,
'preferences',
storageKey,
);
- expect( store.getState().preferences ).toEqual( { chicken: true } );
+ expect( store.getState().preferences ).toEqual( { chicken: true, ribs: true } );
} );
- it( 'should persit to local storage once the state value changes', () => {
+ it( 'should persist to local storage once the state value changes', () => {
const storageKey = 'dumbStorageKey2';
const reducer = ( state, action ) => {
+ if ( action.type === 'REDUX_SERIALIZE' ) {
+ return state;
+ }
+
if ( action.type === 'UPDATE' ) {
return {
preferences: { chicken: true },
@@ -42,6 +47,7 @@ describe( 'loadAndPersist', () => {
const store = createStore( withRehydratation( reducer, 'preferences' ) );
loadAndPersist(
store,
+ reducer,
'preferences',
storageKey,
);
@@ -54,15 +60,13 @@ describe( 'loadAndPersist', () => {
counter: 41,
};
const storageKey = 'dumbStorageKey3';
- const reducer = ( state, action ) => {
+ const reducer = ( state = { preferences: defaultsPreferences }, action ) => {
if ( action.type === 'INCREMENT' ) {
return {
preferences: { counter: state.preferences.counter + 1 },
};
}
- return {
- preferences: { counter: 0 },
- };
+ return state;
};
// store preferences without the `counter` default
@@ -71,9 +75,9 @@ describe( 'loadAndPersist', () => {
const store = createStore( withRehydratation( reducer, 'preferences' ) );
loadAndPersist(
store,
+ reducer,
'preferences',
storageKey,
- defaultsPreferences,
);
store.dispatch( { type: 'INCREMENT' } );
@@ -87,15 +91,13 @@ describe( 'loadAndPersist', () => {
counter: 41,
};
const storageKey = 'dumbStorageKey4';
- const reducer = ( state, action ) => {
+ const reducer = ( state = { preferences: defaultsPreferences }, action ) => {
if ( action.type === 'INCREMENT' ) {
return {
preferences: { counter: state.preferences.counter + 1 },
};
}
- return {
- preferences: { counter: 0 },
- };
+ return state;
};
window.localStorage.setItem( storageKey, JSON.stringify( { counter: 1 } ) );
@@ -104,9 +106,9 @@ describe( 'loadAndPersist', () => {
loadAndPersist(
store,
+ reducer,
'preferences',
storageKey,
- defaultsPreferences,
);
store.dispatch( { type: 'INCREMENT' } );
diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js
index ebefb85ec60939..b545dbbed087d7 100644
--- a/editor/store/test/reducer.js
+++ b/editor/store/test/reducer.js
@@ -24,10 +24,17 @@ import {
notices,
blocksMode,
blockInsertionPoint,
+ isSavingMetaBoxes,
metaBoxes,
reusableBlocks,
} from '../reducer';
+jest.mock( '../../edit-post/meta-boxes', () => {
+ return {
+ getMetaBoxContainer: () => ( { innerHTML: 'meta boxes content' } ),
+ };
+} );
+
describe( 'state', () => {
describe( 'getPostRawValue', () => {
it( 'returns original value for non-rendered content', () => {
@@ -802,6 +809,15 @@ describe( 'state', () => {
expect( state ).toEqual( { start: 'ribs', end: 'ribs', focus: { editable: 'citation' }, isMultiSelecting: true } );
} );
+ it( 'should return same reference if already multi-selecting', () => {
+ const original = deepFreeze( { start: 'ribs', end: 'ribs', focus: { editable: 'citation' }, isMultiSelecting: true } );
+ const state = blockSelection( original, {
+ type: 'START_MULTI_SELECT',
+ } );
+
+ expect( state ).toBe( original );
+ } );
+
it( 'should end multi selection with selection', () => {
const original = deepFreeze( { start: 'ribs', end: 'chicken', focus: { editable: 'citation' }, isMultiSelecting: true } );
const state = blockSelection( original, {
@@ -811,6 +827,15 @@ describe( 'state', () => {
expect( state ).toEqual( { start: 'ribs', end: 'chicken', focus: null, isMultiSelecting: false } );
} );
+ it( 'should return same reference if already ended multi-selecting', () => {
+ const original = deepFreeze( { start: 'ribs', end: 'chicken', focus: null, isMultiSelecting: false } );
+ const state = blockSelection( original, {
+ type: 'STOP_MULTI_SELECT',
+ } );
+
+ expect( state ).toBe( original );
+ } );
+
it( 'should end multi selection without selection', () => {
const original = deepFreeze( { start: 'ribs', end: 'ribs', focus: { editable: 'citation' }, isMultiSelecting: true } );
const state = blockSelection( original, {
@@ -831,7 +856,7 @@ describe( 'state', () => {
expect( state1 ).toBe( original );
} );
- it( 'should unset multi selection and select inserted block', () => {
+ it( 'should unset multi selection', () => {
const original = deepFreeze( { start: 'ribs', end: 'chicken' } );
const state1 = blockSelection( original, {
@@ -839,6 +864,20 @@ describe( 'state', () => {
} );
expect( state1 ).toEqual( { start: null, end: null, focus: null, isMultiSelecting: false } );
+ } );
+
+ it( 'should return same reference if clearing selection but no selection', () => {
+ const original = deepFreeze( { start: null, end: null, focus: null, isMultiSelecting: false } );
+
+ const state1 = blockSelection( original, {
+ type: 'CLEAR_SELECTED_BLOCK',
+ } );
+
+ expect( state1 ).toBe( original );
+ } );
+
+ it( 'should select inserted block', () => {
+ const original = deepFreeze( { start: 'ribs', end: 'chicken' } );
const state3 = blockSelection( original, {
type: 'INSERT_BLOCKS',
@@ -1236,29 +1275,49 @@ describe( 'state', () => {
} );
} );
+ describe( 'isSavingMetaBoxes', () => {
+ it( 'should return default state', () => {
+ const actual = isSavingMetaBoxes( undefined, {} );
+ expect( actual ).toBe( false );
+ } );
+
+ it( 'should set saving flag to true', () => {
+ const action = {
+ type: 'REQUEST_META_BOX_UPDATES',
+ };
+ const actual = isSavingMetaBoxes( false, action );
+
+ expect( actual ).toBe( true );
+ } );
+
+ it( 'should set saving flag to false', () => {
+ const action = {
+ type: 'META_BOX_UPDATES_SUCCESS',
+ };
+ const actual = isSavingMetaBoxes( true, action );
+
+ expect( actual ).toBe( false );
+ } );
+ } );
+
describe( 'metaBoxes()', () => {
it( 'should return default state', () => {
const actual = metaBoxes( undefined, {} );
const expected = {
normal: {
isActive: false,
- isDirty: false,
- isUpdating: false,
},
side: {
isActive: false,
- isDirty: false,
- isUpdating: false,
},
advanced: {
isActive: false,
- isDirty: false,
- isUpdating: false,
},
};
expect( actual ).toEqual( expected );
} );
+
it( 'should set the sidebar to active', () => {
const theMetaBoxes = {
normal: false,
@@ -1275,73 +1334,32 @@ describe( 'state', () => {
const expected = {
normal: {
isActive: false,
- isDirty: false,
- isUpdating: false,
- isLoaded: false,
},
side: {
isActive: true,
- isDirty: false,
- isUpdating: false,
- isLoaded: false,
},
advanced: {
isActive: false,
- isDirty: false,
- isUpdating: false,
- isLoaded: false,
},
};
expect( actual ).toEqual( expected );
} );
- it( 'should switch updating to off', () => {
- const action = {
- type: 'HANDLE_META_BOX_RELOAD',
- location: 'normal',
- };
-
- const theMetaBoxes = metaBoxes( { normal: { isUpdating: true, isActive: false, isDirty: true } }, action );
- const actual = theMetaBoxes.normal;
- const expected = {
- isActive: false,
- isUpdating: false,
- isDirty: false,
- };
-
- expect( actual ).toEqual( expected );
- } );
- it( 'should switch updating to on', () => {
- const action = {
- type: 'REQUEST_META_BOX_UPDATES',
- locations: [ 'normal' ],
- };
-
- const theMetaBoxes = metaBoxes( undefined, action );
- const actual = theMetaBoxes.normal;
- const expected = {
- isActive: false,
- isUpdating: true,
- isDirty: false,
- };
- expect( actual ).toEqual( expected );
- } );
- it( 'should return with the isDirty flag as true', () => {
+ it( 'should set the meta boxes saved data', () => {
const action = {
- type: 'META_BOX_STATE_CHANGED',
- location: 'normal',
- hasChanged: true,
- };
- const theMetaBoxes = metaBoxes( undefined, action );
- const actual = theMetaBoxes.normal;
- const expected = {
- isActive: false,
- isDirty: true,
- isUpdating: false,
+ type: 'META_BOX_SET_SAVED_DATA',
+ dataPerLocation: {
+ side: 'a=b',
+ },
};
- expect( actual ).toEqual( expected );
+ const theMetaBoxes = metaBoxes( { normal: { isActive: true }, side: { isActive: false } }, action );
+ expect( theMetaBoxes ).toEqual( {
+ advanced: { data: undefined },
+ normal: { isActive: true, data: undefined },
+ side: { isActive: false, data: 'a=b' },
+ } );
} );
} );
diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js
index 790932fa3ff2b6..30396887243dd0 100644
--- a/editor/store/test/selectors.js
+++ b/editor/store/test/selectors.js
@@ -7,7 +7,7 @@ import moment from 'moment';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { registerBlockType, unregisterBlockType } from '@wordpress/blocks';
+import { registerBlockType, unregisterBlockType, getBlockTypes } from '@wordpress/blocks';
/**
* Internal dependencies
@@ -70,12 +70,13 @@ import {
didPostSaveRequestFail,
getSuggestedPostFormat,
getNotices,
+ getInserterItems,
getMostFrequentlyUsedBlocks,
- getRecentlyUsedBlocks,
+ getRecentInserterItems,
getMetaBoxes,
- getDirtyMetaBoxes,
+ hasMetaBoxes,
+ isSavingMetaBoxes,
getMetaBox,
- isMetaBoxStateDirty,
getReusableBlock,
isSavingReusableBlock,
isSelectionEnabled,
@@ -97,11 +98,13 @@ describe( 'selectors', () => {
save: ( props ) => props.attributes.text,
category: 'common',
title: 'test block',
+ icon: 'test',
+ keywords: [ 'testing' ],
+ useOnce: true,
} );
} );
beforeEach( () => {
- getDirtyMetaBoxes.clear();
getBlock.clear();
getBlocks.clear();
getEditedPostContent.clear();
@@ -131,174 +134,96 @@ describe( 'selectors', () => {
} );
} );
- describe( 'getDirtyMetaBoxes', () => {
- it( 'should return array of just the side location', () => {
+ describe( 'hasMetaBoxes', () => {
+ it( 'should return true if there are active meta boxes', () => {
const state = {
metaBoxes: {
normal: {
isActive: false,
- isDirty: false,
- isUpdating: false,
},
side: {
isActive: true,
- isDirty: true,
- isUpdating: false,
},
},
};
- expect( getDirtyMetaBoxes( state ) ).toEqual( [ 'side' ] );
+ expect( hasMetaBoxes( state ) ).toBe( true );
} );
- } );
- describe( 'getMetaBoxes', () => {
- it( 'should return the state of all meta boxes', () => {
- const state = {
- metaBoxes: {
- normal: {
- isDirty: false,
- isUpdating: false,
- },
- side: {
- isDirty: false,
- isUpdating: false,
- },
- },
- };
-
- expect( getMetaBoxes( state ) ).toEqual( {
- normal: {
- isDirty: false,
- isUpdating: false,
- },
- side: {
- isDirty: false,
- isUpdating: false,
- },
- } );
- } );
- } );
-
- describe( 'getMetaBox', () => {
- it( 'should return the state of selected meta box', () => {
+ it( 'should return false if there are no active meta boxes', () => {
const state = {
metaBoxes: {
normal: {
isActive: false,
- isDirty: false,
- isUpdating: false,
},
side: {
- isActive: true,
- isDirty: false,
- isUpdating: false,
- },
- },
- };
-
- expect( getMetaBox( state, 'side' ) ).toEqual( {
- isActive: true,
- isDirty: false,
- isUpdating: false,
- } );
- } );
- } );
-
- describe( 'isMetaBoxStateDirty', () => {
- it( 'should return false', () => {
- const state = {
- metaBoxes: {
- normal: {
isActive: false,
- isDirty: false,
- isUpdating: false,
- },
- side: {
- isActive: false,
- isDirty: false,
- isUpdating: false,
},
},
};
- expect( isMetaBoxStateDirty( state ) ).toEqual( false );
+ expect( hasMetaBoxes( state ) ).toBe( false );
} );
+ } );
- it( 'should return false when a dirty meta box is not active.', () => {
+ describe( 'isSavingMetaBoxes', () => {
+ it( 'should return true if some meta boxes are saving', () => {
const state = {
- metaBoxes: {
- normal: {
- isActive: false,
- isDirty: true,
- isUpdating: false,
- },
- side: {
- isActive: false,
- isDirty: false,
- isUpdating: false,
- },
- },
+ isSavingMetaBoxes: true,
};
- expect( isMetaBoxStateDirty( state ) ).toEqual( false );
+ expect( isSavingMetaBoxes( state ) ).toBe( true );
} );
- it( 'should return false when both meta boxes are dirty but inactive.', () => {
+ it( 'should return false if no meta boxes are saving', () => {
const state = {
- metaBoxes: {
- normal: {
- isActive: false,
- isDirty: true,
- isUpdating: false,
- },
- side: {
- isActive: false,
- isDirty: true,
- isUpdating: false,
- },
- },
+ isSavingMetaBoxes: false,
};
- expect( isMetaBoxStateDirty( state ) ).toEqual( false );
+ expect( isSavingMetaBoxes( state ) ).toBe( false );
} );
+ } );
- it( 'should return false when a dirty meta box is active.', () => {
+ describe( 'getMetaBoxes', () => {
+ it( 'should return the state of all meta boxes', () => {
const state = {
metaBoxes: {
normal: {
isActive: true,
- isDirty: true,
- isUpdating: false,
},
side: {
- isActive: false,
- isDirty: false,
- isUpdating: false,
+ isActive: true,
},
},
};
- expect( isMetaBoxStateDirty( state ) ).toEqual( true );
+ expect( getMetaBoxes( state ) ).toEqual( {
+ normal: {
+ isActive: true,
+ },
+ side: {
+ isActive: true,
+ },
+ } );
} );
+ } );
- it( 'should return false when both meta boxes are dirty and active.', () => {
+ describe( 'getMetaBox', () => {
+ it( 'should return the state of selected meta box', () => {
const state = {
metaBoxes: {
normal: {
- isActive: true,
- isDirty: true,
- isUpdating: false,
+ isActive: false,
},
side: {
isActive: true,
- isDirty: true,
- isUpdating: false,
},
},
};
- expect( isMetaBoxStateDirty( state ) ).toEqual( true );
+ expect( getMetaBox( state, 'side' ) ).toEqual( {
+ isActive: true,
+ } );
} );
} );
@@ -579,38 +504,11 @@ describe( 'selectors', () => {
} );
describe( 'isEditedPostDirty', () => {
- const metaBoxes = {
- normal: {
- isActive: false,
- isDirty: false,
- isUpdating: false,
- },
- side: {
- isActive: false,
- isDirty: false,
- isUpdating: false,
- },
- };
- // Those dirty dang meta boxes.
- const dirtyMetaBoxes = {
- normal: {
- isActive: true,
- isDirty: true,
- isUpdating: false,
- },
- side: {
- isActive: false,
- isDirty: false,
- isUpdating: false,
- },
- };
-
it( 'should return true when post saved state dirty', () => {
const state = {
editor: {
isDirty: true,
},
- metaBoxes,
};
expect( isEditedPostDirty( state ) ).toBe( true );
@@ -621,26 +519,14 @@ describe( 'selectors', () => {
editor: {
isDirty: false,
},
- metaBoxes,
};
expect( isEditedPostDirty( state ) ).toBe( false );
} );
-
- it( 'should return true when post saved state not dirty, but meta box state has changed.', () => {
- const state = {
- editor: {
- isDirty: false,
- },
- metaBoxes: dirtyMetaBoxes,
- };
-
- expect( isEditedPostDirty( state ) ).toBe( true );
- } );
} );
describe( 'isCleanNewPost', () => {
- const metaBoxes = { isDirty: false, isUpdating: false };
+ const metaBoxes = {};
it( 'should return true when the post is not dirty and has not been saved before', () => {
const state = {
@@ -837,7 +723,7 @@ describe( 'selectors', () => {
} );
describe( 'getDocumentTitle', () => {
- const metaBoxes = { isDirty: false, isUpdating: false };
+ const metaBoxes = {};
it( 'should return current title unedited existing post', () => {
const state = {
currentPost: {
@@ -1063,7 +949,7 @@ describe( 'selectors', () => {
} );
describe( 'isEditedPostPublishable', () => {
- const metaBoxes = { isDirty: false, isUpdating: false };
+ const metaBoxes = {};
it( 'should return true for pending posts', () => {
const state = {
@@ -2054,20 +1940,33 @@ describe( 'selectors', () => {
saving: {
requesting: true,
},
+ isSavingMetaBoxes: false,
};
expect( isSavingPost( state ) ).toBe( true );
} );
- it( 'should return false if the post is currently being saved', () => {
+ it( 'should return false if the post is not currently being saved', () => {
const state = {
saving: {
requesting: false,
},
+ isSavingMetaBoxes: false,
};
expect( isSavingPost( state ) ).toBe( false );
} );
+
+ it( 'should return true if the post is not currently being saved but meta boxes are saving', () => {
+ const state = {
+ saving: {
+ requesting: false,
+ },
+ isSavingMetaBoxes: true,
+ };
+
+ expect( isSavingPost( state ) ).toBe( true );
+ } );
} );
describe( 'didPostSaveRequestSucceed', () => {
@@ -2248,7 +2147,109 @@ describe( 'selectors', () => {
} );
} );
- describe( 'getRecentlyUsedBlocks', () => {
+ describe( 'getInserterItems', () => {
+ it( 'should list all non-private regular block types', () => {
+ const state = {
+ editor: {
+ present: {
+ blocksByUid: {},
+ blockOrder: [],
+ },
+ },
+ reusableBlocks: {
+ data: {},
+ },
+ };
+
+ const blockTypes = getBlockTypes().filter( blockType => ! blockType.isPrivate );
+ expect( getInserterItems( state ) ).toHaveLength( blockTypes.length );
+ } );
+
+ it( 'should properly list a regular block type', () => {
+ const state = {
+ editor: {
+ present: {
+ blocksByUid: {},
+ blockOrder: [],
+ },
+ },
+ reusableBlocks: {
+ data: {},
+ },
+ };
+
+ expect( getInserterItems( state, [ 'core/test-block' ] ) ).toEqual( [
+ {
+ id: 'core/test-block',
+ name: 'core/test-block',
+ initialAttributes: {},
+ title: 'test block',
+ icon: 'test',
+ category: 'common',
+ keywords: [ 'testing' ],
+ isDisabled: false,
+ },
+ ] );
+ } );
+
+ it( 'should set isDisabled when a regular block type with useOnce has been used', () => {
+ const state = {
+ editor: {
+ present: {
+ blocksByUid: {
+ 1: { uid: 1, name: 'core/test-block', attributes: {} },
+ },
+ blockOrder: [ 1 ],
+ },
+ },
+ reusableBlocks: {
+ data: {},
+ },
+ };
+
+ const items = getInserterItems( state, [ 'core/test-block' ] );
+ expect( items[ 0 ].isDisabled ).toBe( true );
+ } );
+
+ it( 'should properly list reusable blocks', () => {
+ const state = {
+ editor: {
+ present: {
+ blocksByUid: {},
+ blockOrder: [],
+ },
+ },
+ reusableBlocks: {
+ data: {
+ 123: {
+ id: 123,
+ title: 'My reusable block',
+ type: 'core/test-block',
+ },
+ },
+ },
+ };
+
+ expect( getInserterItems( state, [ 'core/block' ] ) ).toEqual( [
+ {
+ id: 'core/block/123',
+ name: 'core/block',
+ initialAttributes: { ref: 123 },
+ title: 'My reusable block',
+ icon: 'test',
+ category: 'reusable-blocks',
+ keywords: [],
+ isDisabled: false,
+ },
+ ] );
+ } );
+
+ it( 'should return nothing when all block types are disabled', () => {
+ expect( getInserterItems( {}, false ) ).toEqual( [] );
+ } );
+ } );
+
+ describe( 'getRecentInserterItems', () => {
it( 'should return the most recently used blocks', () => {
const state = {
preferences: {
@@ -2256,7 +2257,7 @@ describe( 'selectors', () => {
},
};
- expect( getRecentlyUsedBlocks( state ).map( ( block ) => block.name ) )
+ expect( getRecentInserterItems( state ).map( ( item ) => item.name ) )
.toEqual( [ 'core/paragraph', 'core/image' ] );
} );
} );
diff --git a/editor/utils/dom.js b/editor/utils/dom.js
index cfd2df829a0519..c491ca9f1d70e6 100644
--- a/editor/utils/dom.js
+++ b/editor/utils/dom.js
@@ -12,10 +12,11 @@ const { TEXT_NODE } = window.Node;
/**
* Check whether the caret is horizontally at the edge of the container.
*
- * @param {Element} container Focusable element.
- * @param {Boolean} isReverse Set to true to check left, false for right.
- * @param {Boolean} collapseRanges Whether or not to collapse the selection range before the check
- * @return {Boolean} True if at the horizontal edge, false if not.
+ * @param {Element} container Focusable element.
+ * @param {boolean} isReverse Set to true to check left, false for right.
+ * @param {boolean} collapseRanges Whether or not to collapse the selection range before the check.
+ *
+ * @returns {boolean} True if at the horizontal edge, false if not.
*/
export function isHorizontalEdge( container, isReverse, collapseRanges = false ) {
if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) {
@@ -77,10 +78,11 @@ export function isHorizontalEdge( container, isReverse, collapseRanges = false )
/**
* Check whether the caret is vertically at the edge of the container.
*
- * @param {Element} container Focusable element.
- * @param {Boolean} isReverse Set to true to check top, false for bottom.
- * @param {Boolean} collapseRanges Whether or not to collapse the selection range before the check
- * @return {Boolean} True if at the edge, false if not.
+ * @param {Element} container Focusable element.
+ * @param {boolean} isReverse Set to true to check top, false for bottom.
+ * @param {boolean} collapseRanges Whether or not to collapse the selection range before the check.
+ *
+ * @returns {boolean} True if at the edge, false if not.
*/
export function isVerticalEdge( container, isReverse, collapseRanges = false ) {
if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) {
@@ -156,7 +158,7 @@ export function computeCaretRect( container ) {
* Places the caret at start or end of a given element.
*
* @param {Element} container Focusable element.
- * @param {Boolean} isReverse True for end, false for start.
+ * @param {boolean} isReverse True for end, false for start.
*/
export function placeCaretAtHorizontalEdge( container, isReverse ) {
if ( ! container ) {
@@ -198,10 +200,11 @@ export function placeCaretAtHorizontalEdge( container, isReverse ) {
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/caretRangeFromPoint
*
- * @param {Document} doc The document of the range.
- * @param {Float} x Horizontal position within the current viewport.
- * @param {Float} y Vertical position within the current viewport.
- * @return {?Range} The best range for the given point.
+ * @param {Document} doc The document of the range.
+ * @param {number} x Horizontal position within the current viewport.
+ * @param {number} y Vertical position within the current viewport.
+ *
+ * @returns {?Range} The best range for the given point.
*/
function caretRangeFromPoint( doc, x, y ) {
if ( doc.caretRangeFromPoint ) {
@@ -226,11 +229,12 @@ function caretRangeFromPoint( doc, x, y ) {
* Gives the container a temporary high z-index (above any UI).
* This is preferred over getting the UI nodes and set styles there.
*
- * @param {Document} doc The document of the range.
- * @param {Float} x Horizontal position within the current viewport.
- * @param {Float} y Vertical position within the current viewport.
- * @param {Element} container Container in which the range is expected to be found.
- * @return {?Range} The best range for the given point.
+ * @param {Document} doc The document of the range.
+ * @param {number} x Horizontal position within the current viewport.
+ * @param {number} y Vertical position within the current viewport.
+ * @param {Element} container Container in which the range is expected to be found.
+ *
+ * @returns {?Range} The best range for the given point.
*/
function hiddenCaretRangeFromPoint( doc, x, y, container ) {
container.style.zIndex = '10000';
@@ -246,9 +250,9 @@ function hiddenCaretRangeFromPoint( doc, x, y, container ) {
* Places the caret at the top or bottom of a given element.
*
* @param {Element} container Focusable element.
- * @param {Boolean} isReverse True for bottom, false for top.
+ * @param {boolean} isReverse True for bottom, false for top.
* @param {DOMRect} [rect] The rectangle to position the caret with.
- * @param {Boolean} [mayUseScroll=true] True to allow scrolling, false to disallow.
+ * @param {boolean} [mayUseScroll=true] True to allow scrolling, false to disallow.
*/
export function placeCaretAtVerticalEdge( container, isReverse, rect, mayUseScroll = true ) {
if ( ! container ) {
@@ -309,8 +313,9 @@ export function placeCaretAtVerticalEdge( container, isReverse, rect, mayUseScro
/**
* Check whether the given node in an input field.
*
- * @param {HTMLElement} element The HTML element.
- * @return {Boolean} True if the element is an input field, false if not.
+ * @param {HTMLElement} element The HTML element.
+ *
+ * @returns {boolean} True if the element is an input field, false if not.
*/
export function isInputField( { nodeName, contentEditable } ) {
return (
@@ -319,3 +324,20 @@ export function isInputField( { nodeName, contentEditable } ) {
contentEditable === 'true'
);
}
+
+/**
+ * Check wether the current document has a selection.
+ * This checks both for focus in an input field and general text selection.
+ *
+ * @returns {boolean} True if there is selection, false if not.
+ */
+export function documentHasSelection() {
+ if ( isInputField( document.activeElement ) ) {
+ return true;
+ }
+
+ const selection = window.getSelection();
+ const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;
+
+ return range && ! range.collapsed;
+}
diff --git a/editor/utils/mobile/index.js b/editor/utils/mobile/index.js
index ed3dedfa355b78..67e941bb2bd406 100644
--- a/editor/utils/mobile/index.js
+++ b/editor/utils/mobile/index.js
@@ -4,27 +4,11 @@
import { isMobile } from '../../store/selectors';
import { toggleSidebar } from '../../store/actions';
-/**
- * Disables isSidebarOpened on rehydrate payload if the user is on a mobile screen size.
- *
- * @param {Object} payload rehydrate payload
- * @return {Object} rehydrate payload with isSidebarOpened disabled if on mobile
- */
-export const disableIsSidebarOpenedOnMobile = ( payload ) => (
- payload.isSidebarOpenedMobile ? { ...payload, isSidebarOpenedMobile: false } : payload
-);
-
/**
* Middleware
*/
export const mobileMiddleware = ( { getState } ) => next => action => {
- if ( action.type === 'REDUX_REHYDRATE' ) {
- return next( {
- type: 'REDUX_REHYDRATE',
- payload: disableIsSidebarOpenedOnMobile( action.payload ),
- } );
- }
if ( action.type === 'TOGGLE_SIDEBAR' && action.sidebar === undefined ) {
return next( toggleSidebar( isMobile( getState() ) ? 'mobile' : 'desktop', action.forcedValue ) );
}
diff --git a/editor/utils/mobile/test/mobile.js b/editor/utils/mobile/test/mobile.js
deleted file mode 100644
index f13e7ccc752845..00000000000000
--- a/editor/utils/mobile/test/mobile.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Internal dependencies
- */
-import { disableIsSidebarOpenedOnMobile } from '../';
-
-describe( 'disableIsSidebarOpenOnMobile()', () => {
- it( 'should disable isSidebarOpenedMobile and keep other properties as before', () => {
- const input = {
- isSidebarOpenedMobile: true,
- dummyPref: 'dummy',
- },
- output = {
- isSidebarOpenedMobile: false,
- dummyPref: 'dummy',
- };
-
- expect( disableIsSidebarOpenedOnMobile( input ) ).toEqual( output );
- } );
-
- it( 'should keep isSidebarOpenedMobile as false if it was false', () => {
- const input = {
- isSidebarOpenedMobile: false,
- dummy: 'non-dummy',
- },
- output = {
- isSidebarOpenedMobile: false,
- dummy: 'non-dummy',
- };
- expect( disableIsSidebarOpenedOnMobile( input ) ).toEqual( output );
- } );
-
- it( 'should not make any change if the payload does not contain isSidebarOpenedMobile flag', () => {
- const input = {
- isSidebarOpened: true,
- dummy: 'non-dummy',
- },
- output = {
- isSidebarOpened: true,
- dummy: 'non-dummy',
- };
- expect( disableIsSidebarOpenedOnMobile( input ) ).toEqual( output );
- } );
-} );
diff --git a/editor/utils/url.js b/editor/utils/url.js
index b0ce027abae8ec..007cb4fce559cd 100644
--- a/editor/utils/url.js
+++ b/editor/utils/url.js
@@ -4,34 +4,34 @@
import { addQueryArgs } from '@wordpress/url';
/**
- * Returns the Post's Edit URL
+ * Returns the Post's Edit URL.
*
- * @param {Number} postId Post ID
+ * @param {number} postId Post ID.
*
- * @return {String} URL
+ * @returns {string} Post edit URL.
*/
export function getPostEditUrl( postId ) {
return getWPAdminURL( 'post.php', { post: postId, action: 'edit' } );
}
/**
- * Returns the url of a WPAdmin Page
+ * Returns the URL of a WPAdmin Page.
*
- * @param {String} page page to navigate to
- * @param {Object} query Query Args
+ * @param {string} page Page to navigate to.
+ * @param {Object} query Query Args.
*
- * @return {String} URL
+ * @returns {string} WPAdmin URL.
*/
export function getWPAdminURL( page, query ) {
return addQueryArgs( page, query );
}
/**
- * Returns a url for display
+ * Returns a URL for display.
*
- * @param {String} url Original url
+ * @param {string} url Original URL.
*
- * @return {String} Displayed URL
+ * @returns {string} Displayed URL.
*/
export function filterURLForDisplay( url ) {
// remove protocol and www prefixes
diff --git a/editor/utils/with-change-detection/index.js b/editor/utils/with-change-detection/index.js
index 7ed34b586b605a..27063d41b9e86d 100644
--- a/editor/utils/with-change-detection/index.js
+++ b/editor/utils/with-change-detection/index.js
@@ -8,10 +8,11 @@ import { includes } from 'lodash';
* returned reducer will include a new `isDirty` property on the object
* reflecting whether the original reference of the reducer has changed.
*
- * @param {Function} reducer Original reducer
- * @param {?Object} options Optional options
- * @param {?Array} options.resetTypes Action types upon which to reset dirty
- * @return {Function} Enhanced reducer
+ * @param {Function} reducer Original reducer.
+ * @param {?Object} options Optional .
+ * @param {?Array} options.resetTypes Action types upon which to reset dirty.
+ *
+ * @returns {Function} Enhanced reducer.
*/
export default function withChangeDetection( reducer, options = {} ) {
return ( state, action ) => {
diff --git a/editor/utils/with-history/index.js b/editor/utils/with-history/index.js
index 7fa250d1319e79..2335bbc22ee200 100644
--- a/editor/utils/with-history/index.js
+++ b/editor/utils/with-history/index.js
@@ -7,10 +7,11 @@ import { includes } from 'lodash';
* Reducer enhancer which transforms the result of the original reducer into an
* object tracking its own history (past, present, future).
*
- * @param {Function} reducer Original reducer
- * @param {?Object} options Optional options
- * @param {?Array} options.resetTypes Action types upon which to clear past
- * @return {Function} Enhanced reducer
+ * @param {Function} reducer Original reducer.
+ * @param {?Object} options Optional options.
+ * @param {?Array} options.resetTypes Action types upon which to clear past.
+ *
+ * @returns {Function} Enhanced reducer.
*/
export default function withHistory( reducer, options = {} ) {
const initialState = {
diff --git a/element/index.js b/element/index.js
index f850aa80b79553..9f40e5ab22588e 100644
--- a/element/index.js
+++ b/element/index.js
@@ -10,12 +10,13 @@ import { camelCase, flowRight, isString, upperFirst } from 'lodash';
* Returns a new element of given type. Type can be either a string tag name or
* another function which itself returns an element.
*
- * @param {?(string|Function)} type Tag name or element creator
- * @param {Object} props Element properties, either attribute
+ * @param {?(string|Function)} type Tag name or element creator
+ * @param {Object} props Element properties, either attribute
* set to apply to DOM node or values to
* pass through to element creator
- * @param {...WPElement} children Descendant elements
- * @return {WPElement} Element
+ * @param {...WPElement} children Descendant elements
+ *
+ * @returns {WPElement} Element.
*/
export { createElement };
@@ -42,9 +43,10 @@ export { Component };
/**
* Creates a copy of an element with extended props.
*
- * @param {WPElement} element Element
- * @param {?Object} props Props to apply to cloned element
- * @return {WPElement} Cloned element
+ * @param {WPElement} element Element
+ * @param {?Object} props Props to apply to cloned element
+ *
+ * @returns {WPElement} Cloned element.
*/
export { cloneElement };
@@ -74,18 +76,20 @@ export { Fragment };
export { createPortal };
/**
- * Renders a given element into a string
+ * Renders a given element into a string.
+ *
+ * @param {WPElement} element Element to render
*
- * @param {WPElement} element Element to render
- * @return {String} HTML
+ * @returns {String} HTML.
*/
export { renderToStaticMarkup as renderToString };
/**
- * Concatenate two or more React children objects
+ * Concatenate two or more React children objects.
*
- * @param {...?Object} childrenArguments Array of children arguments (array of arrays/strings/objects) to concatenate
- * @return {Array} The concatenated value
+ * @param {...?Object} childrenArguments Array of children arguments (array of arrays/strings/objects) to concatenate.
+ *
+ * @returns {Array} The concatenated value.
*/
export function concatChildren( ...childrenArguments ) {
return childrenArguments.reduce( ( memo, children, i ) => {
@@ -104,11 +108,12 @@ export function concatChildren( ...childrenArguments ) {
}
/**
- * Switches the nodeName of all the elements in the children object
+ * Switches the nodeName of all the elements in the children object.
+ *
+ * @param {?Object} children Children object.
+ * @param {string} nodeName Node name.
*
- * @param {?Object} children Children object
- * @param {String} nodeName Node name
- * @return {?Object} The updated children object
+ * @returns {?Object} The updated children object.
*/
export function switchChildrenNodeName( children, nodeName ) {
return children && Children.map( children, ( elt, index ) => {
@@ -125,7 +130,8 @@ export function switchChildrenNodeName( children, nodeName ) {
* composition, where each successive invocation is supplied the return value of the previous.
*
* @param {...Function} hocs The HOC functions to invoke.
- * @return {Function} Returns the new composite function.
+ *
+ * @returns {Function} Returns the new composite function.
*/
export { flowRight as compose };
@@ -133,9 +139,10 @@ export { flowRight as compose };
* Returns a wrapped version of a React component's display name.
* Higher-order components use getWrapperDisplayName().
*
- * @param {Function|Component} BaseComponent used to detect the existing display name.
- * @param {String} wrapperName Wrapper name to prepend to the display name.
- * @return {String} Wrapped display name.
+ * @param {Function|Component} BaseComponent Used to detect the existing display name.
+ * @param {string} wrapperName Wrapper name to prepend to the display name.
+ *
+ * @returns {string} Wrapped display name.
*/
export function getWrapperDisplayName( BaseComponent, wrapperName ) {
const { displayName = BaseComponent.name || 'Component' } = BaseComponent;
diff --git a/gutenberg.php b/gutenberg.php
index 0336c205a3cbf1..9d5c7e4499bc49 100644
--- a/gutenberg.php
+++ b/gutenberg.php
@@ -2,8 +2,8 @@
/**
* Plugin Name: Gutenberg
* Plugin URI: https://github.com/WordPress/gutenberg
- * Description: Printing since 1440. This is the development plugin for the new block editor in core. Meant for development, do not run on real sites.
- * Version: 1.9.0
+ * Description: Printing since 1440. This is the development plugin for the new block editor in core.
+ * Version: 2.0.0
* Author: Gutenberg Team
*
* @package gutenberg
@@ -109,7 +109,7 @@ function gutenberg_build_files_notice() {
* @since 1.5.0
*/
function gutenberg_pre_init() {
- if ( GUTENBERG_DEVELOPMENT_MODE && ! file_exists( dirname( __FILE__ ) . '/blocks/build' ) ) {
+ if ( defined( 'GUTENBERG_DEVELOPMENT_MODE' ) && GUTENBERG_DEVELOPMENT_MODE && ! file_exists( dirname( __FILE__ ) . '/blocks/build' ) ) {
add_action( 'admin_notices', 'gutenberg_build_files_notice' );
return;
}
@@ -193,13 +193,13 @@ function gutenberg_intercept_edit_post() {
$post = get_post( $post_id );
// Errors and invalid requests are handled in post.php, do not intercept.
- if ( $post ) {
- $post_type = $post->post_type;
- $post_type_object = get_post_type_object( $post_type );
- } else {
+ if ( ! $post ) {
return;
}
+ $post_type = $post->post_type;
+ $post_type_object = get_post_type_object( $post_type );
+
if ( ! $post_type_object ) {
return;
}
diff --git a/i18n/babel-plugin.js b/i18n/babel-plugin.js
index 90d78a92cafa88..670068fc21b148 100644
--- a/i18n/babel-plugin.js
+++ b/i18n/babel-plugin.js
@@ -86,8 +86,9 @@ const REGEXP_TRANSLATOR_COMMENT = /^\s*translators:\s*([\s\S]+)/im;
* Given an argument node (or recursed node), attempts to return a string
* represenation of that node's value.
*
- * @param {Object} node AST node
- * @return {String} String value
+ * @param {Object} node AST node.
+ *
+ * @returns {string} String value.
*/
function getNodeAsString( node ) {
switch ( node.type ) {
@@ -108,10 +109,11 @@ function getNodeAsString( node ) {
/**
* Returns translator comment for a given AST traversal path if one exists.
*
- * @param {Object} path Traversal path
- * @param {Number} _originalNodeLine Private: In recursion, line number of
- * the original node passed
- * @return {?string} Translator comment
+ * @param {Object} path Traversal path.
+ * @param {number} _originalNodeLine Private: In recursion, line number of
+ * the original node passed.
+ *
+ * @returns {?string} Translator comment.
*/
function getTranslatorComment( path, _originalNodeLine ) {
const { node, parent, parentPath } = path;
@@ -158,8 +160,9 @@ function getTranslatorComment( path, _originalNodeLine ) {
* Returns true if the specified key of a function is valid for assignment in
* the translation object.
*
- * @param {string} key Key to test
- * @return {Boolean} Whether key is valid for assignment
+ * @param {string} key Key to test.
+ *
+ * @returns {boolean} Whether key is valid for assignment.
*/
function isValidTranslationKey( key ) {
return -1 !== VALID_TRANSLATION_KEYS.indexOf( key );
@@ -169,9 +172,10 @@ function isValidTranslationKey( key ) {
* Given two translation objects, returns true if valid translation keys match,
* or false otherwise.
*
- * @param {Object} a First translation object
- * @param {Object} b Second translation object
- * @return {Boolean} Whether valid translation keys match
+ * @param {Object} a First translation object.
+ * @param {Object} b Second translation object.
+ *
+ * @returns {boolean} Whether valid translation keys match.
*/
function isSameTranslation( a, b ) {
return isEqual(
diff --git a/i18n/index.js b/i18n/index.js
index f61c35fa7c6501..cb08e164dda1da 100644
--- a/i18n/index.js
+++ b/i18n/index.js
@@ -10,7 +10,7 @@ let i18n;
*
* @see http://messageformat.github.io/Jed/
*
- * @param {Object} data Locale data configuration
+ * @param {Object} data Locale data configuration.
*/
export function setLocaleData( data ) {
i18n = new Jed( data );
@@ -20,7 +20,7 @@ export function setLocaleData( data ) {
* Returns the current Jed instance, initializing with a default configuration
* if not already assigned.
*
- * @return {Jed} Jed instance
+ * @returns {Jed} Jed instance.
*/
export function getI18n() {
if ( ! i18n ) {
@@ -35,8 +35,9 @@ export function getI18n() {
*
* @see https://developer.wordpress.org/reference/functions/__/
*
- * @param {string} text Text to translate
- * @return {string} Translated text
+ * @param {string} text Text to translate.
+ *
+ * @returns {string} Translated text.
*/
export function __( text ) {
return getI18n().gettext( text );
@@ -47,9 +48,10 @@ export function __( text ) {
*
* @see https://developer.wordpress.org/reference/functions/_x/
*
- * @param {string} text Text to translate
- * @param {string} context Context information for the translators
- * @return {string} Translated context string without pipe
+ * @param {string} text Text to translate.
+ * @param {string} context Context information for the translators.
+ *
+ * @returns {string} Translated context string without pipe.
*/
export function _x( text, context ) {
return getI18n().pgettext( context, text );
@@ -61,11 +63,12 @@ export function _x( text, context ) {
*
* @see https://developer.wordpress.org/reference/functions/_n/
*
- * @param {string} single The text to be used if the number is singular
- * @param {string} plural The text to be used if the number is plural
- * @param {Number} number The number to compare against to use either the
- * singular or plural form
- * @return {string} The translated singular or plural form
+ * @param {string} single The text to be used if the number is singular.
+ * @param {string} plural The text to be used if the number is plural.
+ * @param {number} number The number to compare against to use either the
+ * singular or plural form.
+ *
+ * @returns {string} The translated singular or plural form.
*/
export function _n( single, plural, number ) {
return getI18n().ngettext( single, plural, number );
@@ -77,12 +80,13 @@ export function _n( single, plural, number ) {
*
* @see https://developer.wordpress.org/reference/functions/_nx/
*
- * @param {string} single The text to be used if the number is singular
- * @param {string} plural The text to be used if the number is plural
- * @param {Number} number The number to compare against to use either the
- * singular or plural form
- * @param {string} context Context information for the translators
- * @return {string} The translated singular or plural form
+ * @param {string} single The text to be used if the number is singular.
+ * @param {string} plural The text to be used if the number is plural.
+ * @param {number} number The number to compare against to use either the
+ * singular or plural form.
+ * @param {string} context Context information for the translators.
+ *
+ * @returns {string} The translated singular or plural form.
*/
export function _nx( single, plural, number, context ) {
return getI18n().npgettext( context, single, plural, number );
diff --git a/lib/client-assets.php b/lib/client-assets.php
index 5e37b931a93b89..30d8eccec2c61a 100644
--- a/lib/client-assets.php
+++ b/lib/client-assets.php
@@ -79,7 +79,7 @@ function gutenberg_register_scripts_and_styles() {
wp_register_script(
'wp-data',
gutenberg_url( 'data/build/index.js' ),
- array(),
+ array( 'wp-element' ),
filemtime( gutenberg_dir_path() . 'data/build/index.js' )
);
wp_register_script(
@@ -502,6 +502,12 @@ function gutenberg_extend_wp_api_backbone_client() {
wp_json_encode( $schema_response->get_data() )
), 'before' );
}
+
+ /*
+ * For API requests to happen over HTTP/1.0 methods,
+ * as HTTP/1.1 methods are blocked in a variety of situations.
+ */
+ wp_add_inline_script( 'wp-api', 'Backbone.emulateHTTP = true;', 'before' );
}
/**
@@ -684,7 +690,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
wp_enqueue_script(
'wp-editor',
gutenberg_url( 'editor/build/index.js' ),
- array( 'jquery', 'wp-api', 'wp-data', 'wp-date', 'wp-i18n', 'wp-blocks', 'wp-element', 'wp-components', 'wp-utils', 'word-count', 'editor', 'heartbeat' ),
+ array( 'postbox', 'jquery', 'wp-api', 'wp-data', 'wp-date', 'wp-i18n', 'wp-blocks', 'wp-element', 'wp-components', 'wp-utils', 'word-count', 'editor', 'heartbeat' ),
filemtime( gutenberg_dir_path() . 'editor/build/index.js' ),
true // enqueue in the footer.
);
@@ -831,6 +837,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
'post' => $post_to_edit['id'],
'action' => 'edit',
'classic-editor' => true,
+ 'meta_box' => true,
), $meta_box_url );
wp_localize_script( 'wp-editor', '_wpMetaBoxUrl', $meta_box_url );
diff --git a/lib/meta-box-partial-page.php b/lib/meta-box-partial-page.php
index 6a55a062ba0f8c..1fd909fcc228f5 100644
--- a/lib/meta-box-partial-page.php
+++ b/lib/meta-box-partial-page.php
@@ -16,9 +16,8 @@
* @since 1.8.0
*
* @param string $post_type Current post type.
- * @param string $meta_box_context The context location of the meta box. Referred to as context in core.
*/
-function gutenberg_meta_box_save( $post_type, $meta_box_context ) {
+function gutenberg_meta_box_save( $post_type ) {
/**
* Needs classic editor to be active.
*
@@ -45,33 +44,9 @@ function gutenberg_meta_box_save( $post_type, $meta_box_context ) {
return;
}
- /**
- * Prevent over firing of the meta box rendering.
- *
- * The hook do_action( 'do_meta_boxes', ... ) fires three times in
- * edit-form-advanced.php
- *
- * To make sure we properly fire on all three meta box locations, except
- * advanced, as advanced is tied in with normal for ease of use reasons, we
- * need to verify that the action location/context matches our requests
- * meta box location/context. We then exit early if they do not match.
- * This will prevent execution thread from dieing, so the subsequent calls
- * to do_meta_boxes can fire.
- */
- if ( $_REQUEST['meta_box'] !== $meta_box_context ) {
- return;
- }
-
// Ths action is not needed since it's an XHR call.
remove_action( 'admin_head', 'wp_admin_canonical_url' );
-
- $location = $_REQUEST['meta_box'];
-
- if ( ! in_array( $_REQUEST['meta_box'], array( 'side', 'normal', 'advanced' ) ) ) {
- wp_die( __( 'The `meta_box` parameter should be one of "side", "normal", or "advanced".', 'gutenberg' ) );
- }
-
- the_gutenberg_metaboxes( array( $location ) );
+ the_gutenberg_metaboxes();
}
add_action( 'do_meta_boxes', 'gutenberg_meta_box_save', 1000, 2 );
@@ -88,13 +63,10 @@ function gutenberg_meta_box_save( $post_type, $meta_box_context ) {
* @hooked redirect_post_location priority 10
*/
function gutenberg_meta_box_save_redirect( $location, $post_id ) {
- if ( isset( $_REQUEST['gutenberg_meta_boxes'] )
- && isset( $_REQUEST['gutenberg_meta_box_location'] )
- && 'gutenberg_meta_boxes' === $_REQUEST['gutenberg_meta_boxes'] ) {
- $meta_box_location = $_REQUEST['gutenberg_meta_box_location'];
- $location = add_query_arg(
+ if ( isset( $_REQUEST['gutenberg_meta_boxes'] ) ) {
+ $location = add_query_arg(
array(
- 'meta_box' => $meta_box_location,
+ 'meta_box' => true,
'action' => 'edit',
'classic-editor' => true,
'post' => $post_id,
@@ -313,10 +285,8 @@ function gutenberg_show_meta_box_warning( $callback ) {
* Renders the WP meta boxes forms.
*
* @since 1.8.0
- *
- * @param string $locations The metaboxes locations to render.
*/
-function the_gutenberg_metaboxes( $locations = array( 'advanced', 'normal', 'side' ) ) {
+function the_gutenberg_metaboxes() {
global $post, $current_screen, $wp_meta_boxes;
// Handle meta box state.
@@ -334,28 +304,29 @@ function the_gutenberg_metaboxes( $locations = array( 'advanced', 'normal', 'sid
* @param array $wp_meta_boxes Global meta box state.
*/
$wp_meta_boxes = apply_filters( 'filter_gutenberg_meta_boxes', $wp_meta_boxes );
+ $locations = array( 'side', 'normal', 'advanced' );
// Render meta boxes.
- if ( ! empty( $locations ) ) {
- foreach ( $locations as $location ) {
- ?>
-