From ea2a0ae30bf991ebf002065d709eb3dea08c34d3 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 27 Mar 2019 13:04:42 -0400 Subject: [PATCH 01/38] ESLint Plugin: Fix description for valid-sprintf rule (#14666) --- packages/eslint-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index cca141484575cf..4516d123428d68 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -53,7 +53,7 @@ Rule|Description|Recommended [gutenberg-phase](docs/rules/gutenberg-phase.md)|Governs the use of the `process.env.GUTENBERG_PHASE` constant|✓ [no-unused-vars-before-return](/packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md)|Disallow assigning variable values if unused before a return|✓ [react-no-unsafe-timeout](/packages/eslint-plugin/docs/rules/react-no-unsafe-timeout.md)|Disallow unsafe `setTimeout` in component| -[valid-sprintf](/packages/eslint-plugin/docs/rules/valid-sprintf.md)|Disallow assigning variable values if unused before a return|✓ +[valid-sprintf](/packages/eslint-plugin/docs/rules/valid-sprintf.md)|Enforce valid sprintf usage|✓ ### Legacy From d9768ad4294ffc08bed68459d342fa746f63aea4 Mon Sep 17 00:00:00 2001 From: Marek Hrabe Date: Thu, 28 Mar 2019 05:05:55 +0900 Subject: [PATCH 02/38] ResizableBox: Make invisible resize handles bigger (#14481) * make invisible resize handles bigger * add support for all possible handles (even corners) * add comment about functional/visual parts --- assets/stylesheets/_z-index.scss | 6 ++- .../components/src/resizable-box/index.js | 34 +++++++++++++ .../components/src/resizable-box/style.scss | 51 ++++++++++++++----- 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/assets/stylesheets/_z-index.scss b/assets/stylesheets/_z-index.scss index 86ab07f3e28ab1..3c8b921b1fce0c 100644 --- a/assets/stylesheets/_z-index.scss +++ b/assets/stylesheets/_z-index.scss @@ -99,7 +99,11 @@ $z-layers: ( ".nux-dot-tip": 1000001, // Show tooltips above NUX tips, wp-admin menus, submenus, and sidebar: - ".components-tooltip": 1000002 + ".components-tooltip": 1000002, + + // Make sure corner handles are above side handles for ResizableBox component + ".components-resizable-box__side-handle": 1, + ".components-resizable-box__corner-handle": 2 ); @function z-index( $key ) { diff --git a/packages/components/src/resizable-box/index.js b/packages/components/src/resizable-box/index.js index c13129207645c8..13e98a7f4da474 100644 --- a/packages/components/src/resizable-box/index.js +++ b/packages/components/src/resizable-box/index.js @@ -16,6 +16,8 @@ function ResizableBox( { className, ...props } ) { }; const handleClassName = 'components-resizable-box__handle'; + const sideHandleClassName = 'components-resizable-box__side-handle'; + const cornerHandleClassName = 'components-resizable-box__corner-handle'; return ( diff --git a/packages/components/src/resizable-box/style.scss b/packages/components/src/resizable-box/style.scss index 5032d78dc1efbd..433e169ea9dfdc 100644 --- a/packages/components/src/resizable-box/style.scss +++ b/packages/components/src/resizable-box/style.scss @@ -1,16 +1,14 @@ +// This is a wrapper of the actual visible handle (pseudo element). It is styled +// to be much bigger than the visual part so it's easier to click and use. .components-resizable-box__handle { display: none; + width: $resize-handler-container-size; + height: $resize-handler-container-size; // Show the resize handle when selected. .components-resizable-box__container.is-selected & { display: block; } - - // The handle is a pseudo-element and will sit inside this larger - // container size. - width: $resize-handler-container-size; - height: $resize-handler-container-size; - padding: $grid-size-small; } // This is the "visible" resize handle. @@ -23,21 +21,48 @@ border-radius: 50%; background: theme(primary); cursor: inherit; + position: absolute; + top: calc(50% - #{$resize-handler-size / 2}); + right: calc(50% - #{$resize-handler-size / 2}); +} + +// Show corner handles on top of side handles so they can be used +.components-resizable-box__side-handle { + z-index: z-index(".components-resizable-box__side-handle"); +} + +.components-resizable-box__corner-handle { + z-index: z-index(".components-resizable-box__corner-handle"); +} + +// Make horizontal side-handles full width +.components-resizable-box__side-handle.components-resizable-box__handle-top, +.components-resizable-box__side-handle.components-resizable-box__handle-bottom { + width: 100%; + left: 0; +} + +// Make vertical side-handles full height +.components-resizable-box__side-handle.components-resizable-box__handle-left, +.components-resizable-box__side-handle.components-resizable-box__handle-right { + height: 100%; + top: 0; } /*!rtl:begin:ignore*/ .components-resizable-box__handle-right { - top: calc(50% - #{$resize-handler-container-size / 2}); right: calc(#{$resize-handler-container-size / 2} * -1); } -.components-resizable-box__handle-bottom { - bottom: calc(#{$resize-handler-container-size / 2} * -1); - left: calc(50% - #{$resize-handler-container-size / 2}); -} - .components-resizable-box__handle-left { - top: calc(50% - #{$resize-handler-container-size / 2}); left: calc(#{$resize-handler-container-size / 2} * -1); } + +.components-resizable-box__handle-top { + top: calc(#{$resize-handler-container-size / 2} * -1); +} + +.components-resizable-box__handle-bottom { + bottom: calc(#{$resize-handler-container-size / 2} * -1); +} /*!rtl:end:ignore*/ From 1e8d78189a9b05628b171016e102e2c50e267c39 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 28 Mar 2019 08:39:27 +0100 Subject: [PATCH 03/38] Refactor the core/data store to be independent from the registry (#14634) --- lib/client-assets.php | 8 +- lib/packages-dependencies.php | 1 + package-lock.json | 1 + packages/data/package.json | 1 + .../index.js} | 134 +++++++++++------- .../metadata}/actions.js | 30 ++-- .../metadata}/reducer.js | 19 +-- .../metadata}/selectors.js | 35 ++--- .../metadata}/test/reducer.js | 46 ++---- .../metadata}/test/selectors.js | 48 +++---- .../metadata}/test/utils.js | 0 .../metadata}/utils.js | 0 .../test/index.js | 6 +- packages/data/src/plugins/controls/index.js | 35 +---- .../data/src/plugins/persistence/index.js | 24 ++-- packages/data/src/registry.js | 13 +- .../data/src/resolvers-cache-middleware.js | 2 +- packages/data/src/store/index.js | 60 ++++++-- test/unit/__mocks__/@wordpress/data.js | 8 -- 19 files changed, 220 insertions(+), 251 deletions(-) rename packages/data/src/{namespace-store.js => namespace-store/index.js} (64%) rename packages/data/src/{store => namespace-store/metadata}/actions.js (65%) rename packages/data/src/{store => namespace-store/metadata}/reducer.js (77%) rename packages/data/src/{store => namespace-store/metadata}/selectors.js (54%) rename packages/data/src/{store => namespace-store/metadata}/test/reducer.js (66%) rename packages/data/src/{store => namespace-store/metadata}/test/selectors.js (58%) rename packages/data/src/{store => namespace-store/metadata}/test/utils.js (100%) rename packages/data/src/{store => namespace-store/metadata}/utils.js (100%) rename packages/data/src/{plugins/controls => namespace-store}/test/index.js (83%) delete mode 100644 test/unit/__mocks__/@wordpress/data.js diff --git a/lib/client-assets.php b/lib/client-assets.php index 2a97319dd9f1c3..f3ad7c7ee92dfa 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -245,7 +245,9 @@ function gutenberg_register_scripts_and_styles() { ); // TEMPORARY: Core does not (yet) provide persistence migration from the - // introduction of the block editor. + // introduction of the block editor and still calls the data plugins. + // We unset the existing inline scripts first. + $wp_scripts->registered['wp-data']->extra['after'] = array(); wp_add_inline_script( 'wp-data', implode( @@ -254,8 +256,10 @@ function gutenberg_register_scripts_and_styles() { '( function() {', ' var userId = ' . get_current_user_ID() . ';', ' var storageKey = "WP_DATA_USER_" + userId;', + ' wp.data', + ' .use( wp.data.plugins.persistence, { storageKey: storageKey } );', ' wp.data.plugins.persistence.__unstableMigrate( { storageKey: storageKey } );', - '} )()', + '} )();', ) ) ); diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php index fe839ee056704c..3bfbecc382c490 100644 --- a/lib/packages-dependencies.php +++ b/lib/packages-dependencies.php @@ -111,6 +111,7 @@ 'wp-data' => array( 'lodash', 'wp-compose', + 'wp-deprecated', 'wp-element', 'wp-is-shallow-equal', 'wp-priority-queue', diff --git a/package-lock.json b/package-lock.json index 5f6113ab8415f8..6029431dd88175 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2767,6 +2767,7 @@ "requires": { "@babel/runtime": "^7.3.1", "@wordpress/compose": "file:packages/compose", + "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/element": "file:packages/element", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/priority-queue": "file:packages/priority-queue", diff --git a/packages/data/package.json b/packages/data/package.json index d8fde6251944fb..2f05a8cbb499dc 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -24,6 +24,7 @@ "dependencies": { "@babel/runtime": "^7.3.1", "@wordpress/compose": "file:../compose", + "@wordpress/deprecated": "file:../deprecated", "@wordpress/element": "file:../element", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/priority-queue": "file:../priority-queue", diff --git a/packages/data/src/namespace-store.js b/packages/data/src/namespace-store/index.js similarity index 64% rename from packages/data/src/namespace-store.js rename to packages/data/src/namespace-store/index.js index 97f1e2eae1b388..8a53d28b300c3c 100644 --- a/packages/data/src/namespace-store.js +++ b/packages/data/src/namespace-store/index.js @@ -7,12 +7,21 @@ import { get, mapValues, } from 'lodash'; +import combineReducers from 'turbo-combine-reducers'; + +/** + * WordPress dependencies + */ +import createReduxRoutineMiddleware from '@wordpress/redux-routine'; /** * Internal dependencies */ -import promise from './promise-middleware'; -import createResolversCacheMiddleware from './resolvers-cache-middleware'; +import promise from '../promise-middleware'; +import createResolversCacheMiddleware from '../resolvers-cache-middleware'; +import metadataReducer from './metadata/reducer'; +import * as metadataSelectors from './metadata/selectors'; +import * as metadataActions from './metadata/actions'; /** * Creates a namespace object with a store derived from the reducer given. @@ -27,16 +36,27 @@ export default function createNamespace( key, options, registry ) { const reducer = options.reducer; const store = createReduxStore( key, options, registry ); - let selectors, actions, resolvers; - if ( options.actions ) { - actions = mapActions( options.actions, store ); - } - if ( options.selectors ) { - selectors = mapSelectors( options.selectors, store, registry ); - } + let resolvers; + const actions = mapActions( { + ...metadataActions, + ...options.actions, + }, store ); + let selectors = mapSelectors( { + ...mapValues( metadataSelectors, ( selector ) => ( state, ...args ) => selector( state.metadata, ...args ) ), + ...mapValues( options.selectors, ( selector ) => { + if ( selector.isRegistrySelector ) { + const mappedSelector = ( reg ) => ( state, ...args ) => { + return selector( reg )( state.root, ...args ); + }; + mappedSelector.isRegistrySelector = selector.isRegistrySelector; + return mappedSelector; + } + + return ( state, ...args ) => selector( state.root, ...args ); + } ), + }, store, registry ); if ( options.resolvers ) { - const fulfillment = getCoreDataFulfillment( registry, key ); - const result = mapResolvers( options.resolvers, selectors, fulfillment, store ); + const result = mapResolvers( options.resolvers, selectors, store ); resolvers = result.resolvers; selectors = result.selectors; } @@ -44,12 +64,18 @@ export default function createNamespace( key, options, registry ) { const getSelectors = () => selectors; const getActions = () => actions; + // We have some modules monkey-patching the store object + // It's wrong to do so but until we refactor all of our effects to controls + // We need to keep the same "store" instance here. + store.__unstableOriginalGetState = store.getState; + store.getState = () => store.__unstableOriginalGetState().root; + // Customize subscribe behavior to call listeners only on effective change, // not on every dispatch. const subscribe = store && function( listener ) { - let lastState = store.getState(); + let lastState = store.__unstableOriginalGetState(); store.subscribe( () => { - const state = store.getState(); + const state = store.__unstableOriginalGetState(); const hasChanged = state !== lastState; lastState = state; @@ -84,15 +110,36 @@ export default function createNamespace( key, options, registry ) { * @return {Object} Newly created redux store. */ function createReduxStore( key, options, registry ) { + const middlewares = [ + createResolversCacheMiddleware( registry, key ), + promise, + ]; + + if ( options.controls ) { + const normalizedControls = mapValues( options.controls, ( control ) => { + return control.isRegistryControl ? control( registry ) : control; + } ); + middlewares.push( createReduxRoutineMiddleware( normalizedControls ) ); + } + const enhancers = [ - applyMiddleware( createResolversCacheMiddleware( registry, key ), promise ), + applyMiddleware( ...middlewares ), ]; if ( typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__ ) { enhancers.push( window.__REDUX_DEVTOOLS_EXTENSION__( { name: key, instanceId: key } ) ); } const { reducer, initialState } = options; - return createStore( reducer, initialState, flowRight( enhancers ) ); + const enhancedReducer = combineReducers( { + metadata: metadataReducer, + root: reducer, + } ); + + return createStore( + enhancedReducer, + { root: initialState }, + flowRight( enhancers ) + ); } /** @@ -120,7 +167,7 @@ function mapSelectors( selectors, store, registry ) { // direct assignment. const argsLength = arguments.length; const args = new Array( argsLength + 1 ); - args[ 0 ] = store.getState(); + args[ 0 ] = store.__unstableOriginalGetState(); for ( let i = 0; i < argsLength; i++ ) { args[ i + 1 ] = arguments[ i ]; } @@ -151,11 +198,15 @@ function mapActions( actions, store ) { * * @param {Object} resolvers Resolvers to register. * @param {Object} selectors The current selectors to be modified. - * @param {Object} fulfillment Fulfillment implementation functions. * @param {Object} store The redux store to which the resolvers should be mapped. * @return {Object} An object containing updated selectors and resolvers. */ -function mapResolvers( resolvers, selectors, fulfillment, store ) { +function mapResolvers( resolvers, selectors, store ) { + const mappedResolvers = mapValues( resolvers, ( resolver ) => { + const { fulfill: resolverFulfill = resolver } = resolver; + return { ...resolver, fulfill: resolverFulfill }; + } ); + const mapSelector = ( selector, selectorName ) => { const resolver = resolvers[ selectorName ]; if ( ! resolver ) { @@ -169,13 +220,14 @@ function mapResolvers( resolvers, selectors, fulfillment, store ) { return; } - if ( fulfillment.hasStarted( selectorName, args ) ) { + const { metadata } = store.__unstableOriginalGetState(); + if ( metadataSelectors.hasStartedResolution( metadata, selectorName, args ) ) { return; } - fulfillment.start( selectorName, args ); - await fulfillment.fulfill( selectorName, ...args ); - fulfillment.finish( selectorName, args ); + store.dispatch( metadataActions.startResolution( selectorName, args ) ); + await fulfillResolver( store, mappedResolvers, selectorName, ...args ); + store.dispatch( metadataActions.finishResolution( selectorName, args ) ); } fulfillSelector( ...args ); @@ -183,54 +235,28 @@ function mapResolvers( resolvers, selectors, fulfillment, store ) { }; }; - const mappedResolvers = mapValues( resolvers, ( resolver ) => { - const { fulfill: resolverFulfill = resolver } = resolver; - return { ...resolver, fulfill: resolverFulfill }; - } ); - return { resolvers: mappedResolvers, selectors: mapValues( selectors, mapSelector ), }; } -/** - * Bundles up fulfillment functions for resolvers. - * @param {Object} registry Registry reference, for fulfilling via resolvers - * @param {string} key Part of the state shape to register the - * selectors for. - * @return {Object} An object providing fulfillment functions. - */ -function getCoreDataFulfillment( registry, key ) { - const { hasStartedResolution } = registry.select( 'core/data' ); - const { startResolution, finishResolution } = registry.dispatch( 'core/data' ); - - return { - hasStarted: ( ...args ) => hasStartedResolution( key, ...args ), - start: ( ...args ) => startResolution( key, ...args ), - finish: ( ...args ) => finishResolution( key, ...args ), - fulfill: ( ...args ) => fulfillWithRegistry( registry, key, ...args ), - }; -} - /** * Calls a resolver given arguments * - * @param {Object} registry Registry reference, for fulfilling via resolvers - * @param {string} key Part of the state shape to register the - * selectors for. + * @param {Object} store Store reference, for fulfilling via resolvers + * @param {Object} resolvers Store Resolvers * @param {string} selectorName Selector name to fulfill. - * @param {Array} args Selector Arguments. + * @param {Array} args Selector Arguments. */ -async function fulfillWithRegistry( registry, key, selectorName, ...args ) { - const namespace = registry.stores[ key ]; - const resolver = get( namespace, [ 'resolvers', selectorName ] ); +async function fulfillResolver( store, resolvers, selectorName, ...args ) { + const resolver = get( resolvers, [ selectorName ] ); if ( ! resolver ) { return; } const action = resolver.fulfill( ...args ); if ( action ) { - await namespace.store.dispatch( action ); + await store.dispatch( action ); } } diff --git a/packages/data/src/store/actions.js b/packages/data/src/namespace-store/metadata/actions.js similarity index 65% rename from packages/data/src/store/actions.js rename to packages/data/src/namespace-store/metadata/actions.js index b7bd9aa805738f..528cce18cd39f1 100644 --- a/packages/data/src/store/actions.js +++ b/packages/data/src/namespace-store/metadata/actions.js @@ -2,16 +2,14 @@ * Returns an action object used in signalling that selector resolution has * started. * - * @param {string} reducerKey Registered store reducer key. * @param {string} selectorName Name of selector for which resolver triggered. * @param {...*} args Arguments to associate for uniqueness. * * @return {Object} Action object. */ -export function startResolution( reducerKey, selectorName, args ) { +export function startResolution( selectorName, args ) { return { type: 'START_RESOLUTION', - reducerKey, selectorName, args, }; @@ -21,16 +19,14 @@ export function startResolution( reducerKey, selectorName, args ) { * Returns an action object used in signalling that selector resolution has * completed. * - * @param {string} reducerKey Registered store reducer key. * @param {string} selectorName Name of selector for which resolver triggered. * @param {...*} args Arguments to associate for uniqueness. * * @return {Object} Action object. */ -export function finishResolution( reducerKey, selectorName, args ) { +export function finishResolution( selectorName, args ) { return { type: 'FINISH_RESOLUTION', - reducerKey, selectorName, args, }; @@ -39,53 +35,43 @@ export function finishResolution( reducerKey, selectorName, args ) { /** * Returns an action object used in signalling that we should invalidate the resolution cache. * - * @param {string} reducerKey Registered store reducer key. * @param {string} selectorName Name of selector for which resolver should be invalidated. * @param {Array} args Arguments to associate for uniqueness. * * @return {Object} Action object. */ -export function invalidateResolution( reducerKey, selectorName, args ) { +export function invalidateResolution( selectorName, args ) { return { type: 'INVALIDATE_RESOLUTION', - reducerKey, selectorName, args, }; } /** - * Returns an action object used in signalling that the resolution cache for a - * given reducerKey should be invalidated. - * - * @param {string} reducerKey Registered store reducer key. + * Returns an action object used in signalling that the resolution + * should be invalidated. * * @return {Object} Action object. */ -export function invalidateResolutionForStore( reducerKey ) { +export function invalidateResolutionForStore() { return { type: 'INVALIDATE_RESOLUTION_FOR_STORE', - reducerKey, }; } /** * Returns an action object used in signalling that the resolution cache for a - * given reducerKey and selectorName should be invalidated. + * given selectorName should be invalidated. * - * @param {string} reducerKey Registered store reducer key. * @param {string} selectorName Name of selector for which all resolvers should * be invalidated. * * @return {Object} Action object. */ -export function invalidateResolutionForStoreSelector( - reducerKey, - selectorName -) { +export function invalidateResolutionForStoreSelector( selectorName ) { return { type: 'INVALIDATE_RESOLUTION_FOR_STORE_SELECTOR', - reducerKey, selectorName, }; } diff --git a/packages/data/src/store/reducer.js b/packages/data/src/namespace-store/metadata/reducer.js similarity index 77% rename from packages/data/src/store/reducer.js rename to packages/data/src/namespace-store/metadata/reducer.js index cd043f753f833b..46ddc183563ae0 100644 --- a/packages/data/src/store/reducer.js +++ b/packages/data/src/namespace-store/metadata/reducer.js @@ -13,7 +13,7 @@ import { onSubKey } from './utils'; * Reducer function returning next state for selector resolution of * subkeys, object form: * - * reducerKey -> selectorName -> EquivalentKeyMap + * selectorName -> EquivalentKeyMap * * @param {Object} state Current state. * @param {Object} action Dispatched action. @@ -21,7 +21,6 @@ import { onSubKey } from './utils'; * @returns {Object} Next state. */ const subKeysIsResolved = flowRight( [ - onSubKey( 'reducerKey' ), onSubKey( 'selectorName' ), ] )( ( state = new EquivalentKeyMap(), action ) => { switch ( action.type ) { @@ -44,7 +43,7 @@ const subKeysIsResolved = flowRight( [ /** * Reducer function returning next state for selector resolution, object form: * - * reducerKey -> selectorName -> EquivalentKeyMap + * selectorName -> EquivalentKeyMap * * @param {Object} state Current state. * @param {Object} action Dispatched action. @@ -54,18 +53,10 @@ const subKeysIsResolved = flowRight( [ const isResolved = ( state = {}, action ) => { switch ( action.type ) { case 'INVALIDATE_RESOLUTION_FOR_STORE': - return has( state, action.reducerKey ) ? - omit( state, [ action.reducerKey ] ) : - state; + return {}; case 'INVALIDATE_RESOLUTION_FOR_STORE_SELECTOR': - return has( state, [ action.reducerKey, action.selectorName ] ) ? - { - ...state, - [ action.reducerKey ]: omit( - state[ action.reducerKey ], - [ action.selectorName ] - ), - } : + return has( state, [ action.selectorName ] ) ? + omit( state, [ action.selectorName ] ) : state; case 'START_RESOLUTION': case 'FINISH_RESOLUTION': diff --git a/packages/data/src/store/selectors.js b/packages/data/src/namespace-store/metadata/selectors.js similarity index 54% rename from packages/data/src/store/selectors.js rename to packages/data/src/namespace-store/metadata/selectors.js index b17594e6705a5d..34217edcedd10f 100644 --- a/packages/data/src/store/selectors.js +++ b/packages/data/src/namespace-store/metadata/selectors.js @@ -4,20 +4,19 @@ import { get } from 'lodash'; /** - * Returns the raw `isResolving` value for a given reducer key, selector name, + * Returns the raw `isResolving` value for a given selector name, * and arguments set. May be undefined if the selector has never been resolved * or not resolved for the given set of arguments, otherwise true or false for * resolution started and completed respectively. * * @param {Object} state Data state. - * @param {string} reducerKey Registered store reducer key. * @param {string} selectorName Selector name. * @param {Array} args Arguments passed to selector. * * @return {?boolean} isResolving value. */ -export function getIsResolving( state, reducerKey, selectorName, args ) { - const map = get( state, [ reducerKey, selectorName ] ); +export function getIsResolving( state, selectorName, args ) { + const map = get( state, [ selectorName ] ); if ( ! map ) { return; } @@ -26,58 +25,54 @@ export function getIsResolving( state, reducerKey, selectorName, args ) { } /** - * Returns true if resolution has already been triggered for a given reducer - * key, selector name, and arguments set. + * Returns true if resolution has already been triggered for a given + * selector name, and arguments set. * * @param {Object} state Data state. - * @param {string} reducerKey Registered store reducer key. * @param {string} selectorName Selector name. * @param {?Array} args Arguments passed to selector (default `[]`). * * @return {boolean} Whether resolution has been triggered. */ -export function hasStartedResolution( state, reducerKey, selectorName, args = [] ) { - return getIsResolving( state, reducerKey, selectorName, args ) !== undefined; +export function hasStartedResolution( state, selectorName, args = [] ) { + return getIsResolving( state, selectorName, args ) !== undefined; } /** - * Returns true if resolution has completed for a given reducer key, selector + * Returns true if resolution has completed for a given selector * name, and arguments set. * * @param {Object} state Data state. - * @param {string} reducerKey Registered store reducer key. * @param {string} selectorName Selector name. * @param {?Array} args Arguments passed to selector. * * @return {boolean} Whether resolution has completed. */ -export function hasFinishedResolution( state, reducerKey, selectorName, args = [] ) { - return getIsResolving( state, reducerKey, selectorName, args ) === false; +export function hasFinishedResolution( state, selectorName, args = [] ) { + return getIsResolving( state, selectorName, args ) === false; } /** * Returns true if resolution has been triggered but has not yet completed for - * a given reducer key, selector name, and arguments set. + * a given selector name, and arguments set. * * @param {Object} state Data state. - * @param {string} reducerKey Registered store reducer key. * @param {string} selectorName Selector name. * @param {?Array} args Arguments passed to selector. * * @return {boolean} Whether resolution is in progress. */ -export function isResolving( state, reducerKey, selectorName, args = [] ) { - return getIsResolving( state, reducerKey, selectorName, args ) === true; +export function isResolving( state, selectorName, args = [] ) { + return getIsResolving( state, selectorName, args ) === true; } /** * Returns the list of the cached resolvers. * * @param {Object} state Data state. - * @param {string} reducerKey Registered store reducer key. * * @return {Object} Resolvers mapped by args and selectorName. */ -export function getCachedResolvers( state, reducerKey ) { - return state.hasOwnProperty( reducerKey ) ? state[ reducerKey ] : {}; +export function getCachedResolvers( state ) { + return state; } diff --git a/packages/data/src/store/test/reducer.js b/packages/data/src/namespace-store/metadata/test/reducer.js similarity index 66% rename from packages/data/src/store/test/reducer.js rename to packages/data/src/namespace-store/metadata/test/reducer.js index eab156e89a71e4..d1af3f7804ea9c 100644 --- a/packages/data/src/store/test/reducer.js +++ b/packages/data/src/namespace-store/metadata/test/reducer.js @@ -18,128 +18,106 @@ describe( 'reducer', () => { it( 'should return with started resolution', () => { const state = reducer( undefined, { type: 'START_RESOLUTION', - reducerKey: 'test', selectorName: 'getFoo', args: [], } ); // { test: { getFoo: EquivalentKeyMap( [] => true ) } } - expect( state.test.getFoo.get( [] ) ).toBe( true ); + expect( state.getFoo.get( [] ) ).toBe( true ); } ); it( 'should return with finished resolution', () => { const original = reducer( undefined, { type: 'START_RESOLUTION', - reducerKey: 'test', selectorName: 'getFoo', args: [], } ); const state = reducer( deepFreeze( original ), { type: 'FINISH_RESOLUTION', - reducerKey: 'test', selectorName: 'getFoo', args: [], } ); // { test: { getFoo: EquivalentKeyMap( [] => false ) } } - expect( state.test.getFoo.get( [] ) ).toBe( false ); + expect( state.getFoo.get( [] ) ).toBe( false ); } ); it( 'should remove invalidations', () => { let state = reducer( undefined, { type: 'START_RESOLUTION', - reducerKey: 'test', selectorName: 'getFoo', args: [], } ); state = reducer( deepFreeze( state ), { type: 'FINISH_RESOLUTION', - reducerKey: 'test', selectorName: 'getFoo', args: [], } ); state = reducer( deepFreeze( state ), { type: 'INVALIDATE_RESOLUTION', - reducerKey: 'test', selectorName: 'getFoo', args: [], } ); - // { test: { getFoo: EquivalentKeyMap( [] => undefined ) } } - expect( state.test.getFoo.get( [] ) ).toBe( undefined ); + // { getFoo: EquivalentKeyMap( [] => undefined ) } + expect( state.getFoo.get( [] ) ).toBe( undefined ); } ); it( 'different arguments should not conflict', () => { const original = reducer( undefined, { type: 'START_RESOLUTION', - reducerKey: 'test', selectorName: 'getFoo', args: [ 'post' ], } ); let state = reducer( deepFreeze( original ), { type: 'FINISH_RESOLUTION', - reducerKey: 'test', selectorName: 'getFoo', args: [ 'post' ], } ); state = reducer( deepFreeze( state ), { type: 'START_RESOLUTION', - reducerKey: 'test', selectorName: 'getFoo', args: [ 'block' ], } ); - // { test: { getFoo: EquivalentKeyMap( [] => false ) } } - expect( state.test.getFoo.get( [ 'post' ] ) ).toBe( false ); - expect( state.test.getFoo.get( [ 'block' ] ) ).toBe( true ); + // { getFoo: EquivalentKeyMap( [] => false ) } + expect( state.getFoo.get( [ 'post' ] ) ).toBe( false ); + expect( state.getFoo.get( [ 'block' ] ) ).toBe( true ); } ); it( 'should remove invalidation for store level and leave others ' + 'intact', () => { const original = reducer( undefined, { type: 'FINISH_RESOLUTION', - reducerKey: 'testA', selectorName: 'getFoo', args: [ 'post' ], } ); - let state = reducer( deepFreeze( original ), { - type: 'FINISH_RESOLUTION', - reducerKey: 'testB', - selectorName: 'getBar', - args: [ 'postBar' ], - } ); - state = reducer( deepFreeze( state ), { + const state = reducer( deepFreeze( original ), { type: 'INVALIDATE_RESOLUTION_FOR_STORE', - reducerKey: 'testA', } ); - expect( state.testA ).toBeUndefined(); - // { testB: { getBar: EquivalentKeyMap( [] => false ) } } - expect( state.testB.getBar.get( [ 'postBar' ] ) ).toBe( false ); + expect( state ).toEqual( {} ); } ); it( 'should remove invalidation for store and selector name level and ' + 'leave other selectors at store level intact', () => { const original = reducer( undefined, { type: 'FINISH_RESOLUTION', - reducerKey: 'test', selectorName: 'getFoo', args: [ 'post' ], } ); let state = reducer( deepFreeze( original ), { type: 'FINISH_RESOLUTION', - reducerKey: 'test', selectorName: 'getBar', args: [ 'postBar' ], } ); state = reducer( deepFreeze( state ), { type: 'INVALIDATE_RESOLUTION_FOR_STORE_SELECTOR', - reducerKey: 'test', selectorName: 'getBar', } ); - expect( state.test.getBar ).toBeUndefined(); - // { test: { getFoo: EquivalentKeyMap( [] => false ) } } - expect( state.test.getFoo.get( [ 'post' ] ) ).toBe( false ); + expect( state.getBar ).toBeUndefined(); + // { getFoo: EquivalentKeyMap( [] => false ) } + expect( state.getFoo.get( [ 'post' ] ) ).toBe( false ); } ); } ); diff --git a/packages/data/src/store/test/selectors.js b/packages/data/src/namespace-store/metadata/test/selectors.js similarity index 58% rename from packages/data/src/store/test/selectors.js rename to packages/data/src/namespace-store/metadata/test/selectors.js index 1af4bf9b8adf19..916d3beb436312 100644 --- a/packages/data/src/store/test/selectors.js +++ b/packages/data/src/namespace-store/metadata/test/selectors.js @@ -16,29 +16,25 @@ import { describe( 'getIsResolving', () => { it( 'should return undefined if no state by reducerKey, selectorName', () => { const state = {}; - const result = getIsResolving( state, 'test', 'getFoo', [] ); + const result = getIsResolving( state, 'getFoo', [] ); expect( result ).toBe( undefined ); } ); it( 'should return undefined if state by reducerKey, selectorName, but not args', () => { const state = { - test: { - getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), - }, + getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), }; - const result = getIsResolving( state, 'test', 'getFoo', [ 'bar' ] ); + const result = getIsResolving( state, 'getFoo', [ 'bar' ] ); expect( result ).toBe( undefined ); } ); it( 'should return value by reducerKey, selectorName', () => { const state = { - test: { - getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), - }, + getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), }; - const result = getIsResolving( state, 'test', 'getFoo', [] ); + const result = getIsResolving( state, 'getFoo', [] ); expect( result ).toBe( true ); } ); @@ -47,18 +43,16 @@ describe( 'getIsResolving', () => { describe( 'hasStartedResolution', () => { it( 'returns false if not has started', () => { const state = {}; - const result = hasStartedResolution( state, 'test', 'getFoo', [] ); + const result = hasStartedResolution( state, 'getFoo', [] ); expect( result ).toBe( false ); } ); it( 'returns true if has started', () => { const state = { - test: { - getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), - }, + getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), }; - const result = hasStartedResolution( state, 'test', 'getFoo', [] ); + const result = hasStartedResolution( state, 'getFoo', [] ); expect( result ).toBe( true ); } ); @@ -67,22 +61,18 @@ describe( 'hasStartedResolution', () => { describe( 'hasFinishedResolution', () => { it( 'returns false if not has finished', () => { const state = { - test: { - getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), - }, + getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), }; - const result = hasFinishedResolution( state, 'test', 'getFoo', [] ); + const result = hasFinishedResolution( state, 'getFoo', [] ); expect( result ).toBe( false ); } ); it( 'returns true if has finished', () => { const state = { - test: { - getFoo: new EquivalentKeyMap( [ [ [], false ] ] ), - }, + getFoo: new EquivalentKeyMap( [ [ [], false ] ] ), }; - const result = hasFinishedResolution( state, 'test', 'getFoo', [] ); + const result = hasFinishedResolution( state, 'getFoo', [] ); expect( result ).toBe( true ); } ); @@ -91,29 +81,25 @@ describe( 'hasFinishedResolution', () => { describe( 'isResolving', () => { it( 'returns false if not has started', () => { const state = {}; - const result = isResolving( state, 'test', 'getFoo', [] ); + const result = isResolving( state, 'getFoo', [] ); expect( result ).toBe( false ); } ); it( 'returns false if has finished', () => { const state = { - test: { - getFoo: new EquivalentKeyMap( [ [ [], false ] ] ), - }, + getFoo: new EquivalentKeyMap( [ [ [], false ] ] ), }; - const result = isResolving( state, 'test', 'getFoo', [] ); + const result = isResolving( state, 'getFoo', [] ); expect( result ).toBe( false ); } ); it( 'returns true if has started but not finished', () => { const state = { - test: { - getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), - }, + getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), }; - const result = isResolving( state, 'test', 'getFoo', [] ); + const result = isResolving( state, 'getFoo', [] ); expect( result ).toBe( true ); } ); diff --git a/packages/data/src/store/test/utils.js b/packages/data/src/namespace-store/metadata/test/utils.js similarity index 100% rename from packages/data/src/store/test/utils.js rename to packages/data/src/namespace-store/metadata/test/utils.js diff --git a/packages/data/src/store/utils.js b/packages/data/src/namespace-store/metadata/utils.js similarity index 100% rename from packages/data/src/store/utils.js rename to packages/data/src/namespace-store/metadata/utils.js diff --git a/packages/data/src/plugins/controls/test/index.js b/packages/data/src/namespace-store/test/index.js similarity index 83% rename from packages/data/src/plugins/controls/test/index.js rename to packages/data/src/namespace-store/test/index.js index d60cb9053211d9..eba9e7e6697fc5 100644 --- a/packages/data/src/plugins/controls/test/index.js +++ b/packages/data/src/namespace-store/test/index.js @@ -1,16 +1,14 @@ /** * Internal dependencies */ -import { createRegistry } from '../../../registry'; -import { createRegistryControl } from '../../../factory'; -import controlsPlugin from '../'; +import { createRegistry } from '../../registry'; +import { createRegistryControl } from '../../factory'; describe( 'controls', () => { let registry; beforeEach( () => { registry = createRegistry(); - registry.use( controlsPlugin ); } ); describe( 'should call registry-aware controls', () => { diff --git a/packages/data/src/plugins/controls/index.js b/packages/data/src/plugins/controls/index.js index a023dd6916b107..f5dd906dde60e7 100644 --- a/packages/data/src/plugins/controls/index.js +++ b/packages/data/src/plugins/controls/index.js @@ -1,36 +1,11 @@ -/** - * External dependencies - */ -import { applyMiddleware } from 'redux'; -import { mapValues } from 'lodash'; - /** * WordPress dependencies */ -import createMiddleware from '@wordpress/redux-routine'; +import deprecated from '@wordpress/deprecated'; export default function( registry ) { - return { - registerStore( reducerKey, options ) { - const store = registry.registerStore( reducerKey, options ); - - if ( options.controls ) { - const normalizedControls = mapValues( options.controls, ( control ) => { - return control.isRegistryControl ? control( registry ) : control; - } ); - const middleware = createMiddleware( normalizedControls ); - const enhancer = applyMiddleware( middleware ); - const createStore = () => store; - - Object.assign( - store, - enhancer( createStore )( options.reducer ) - ); - - registry.namespaces[ reducerKey ].supportControls = true; - } - - return store; - }, - }; + deprecated( 'wp.data.plugins.controls', { + hint: 'The controls plugins is now baked-in.', + } ); + return registry; } diff --git a/packages/data/src/plugins/persistence/index.js b/packages/data/src/plugins/persistence/index.js index 5ce7f41a37c2fc..4c9f5c7c0f2b97 100644 --- a/packages/data/src/plugins/persistence/index.js +++ b/packages/data/src/plugins/persistence/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { flow, merge, isPlainObject, omit } from 'lodash'; +import { merge, isPlainObject, omit } from 'lodash'; /** * Internal dependencies @@ -147,14 +147,12 @@ const persistencePlugin = function( registry, pluginOptions ) { let lastState = getPersistedState( undefined, { nextState: getState() } ); - return ( result ) => { + return () => { const state = getPersistedState( lastState, { nextState: getState() } ); if ( state !== lastState ) { persistence.set( reducerKey, state ); lastState = state; } - - return result; }; } @@ -184,19 +182,19 @@ const persistencePlugin = function( registry, pluginOptions ) { initialState = persistedState; } - options = { ...options, initialState }; + options = { + ...options, + initialState, + }; } const store = registry.registerStore( reducerKey, options ); - store.dispatch = flow( [ - store.dispatch, - createPersistOnChange( - store.getState, - reducerKey, - options.persist - ), - ] ); + store.subscribe( createPersistOnChange( + store.getState, + reducerKey, + options.persist + ) ); return store; }, diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index acca73b93129a3..e1c324f6ef8ac0 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -9,8 +9,8 @@ import { /** * Internal dependencies */ -import createNamespace from './namespace-store.js'; -import dataStore from './store'; +import createNamespace from './namespace-store'; +import createCoreDataStore from './store'; /** * An isolated orchestrator of store registrations. @@ -166,10 +166,11 @@ export function createRegistry( storeConfigs = {} ) { return registry; } - Object.entries( { - 'core/data': dataStore, - ...storeConfigs, - } ).map( ( [ name, config ] ) => registry.registerStore( name, config ) ); + registerGenericStore( 'core/data', createCoreDataStore( registry ) ); + + Object.entries( storeConfigs ).forEach( + ( [ name, config ] ) => registry.registerStore( name, config ) + ); return withPlugins( registry ); } diff --git a/packages/data/src/resolvers-cache-middleware.js b/packages/data/src/resolvers-cache-middleware.js index 0bc57390795669..7477b36d805b59 100644 --- a/packages/data/src/resolvers-cache-middleware.js +++ b/packages/data/src/resolvers-cache-middleware.js @@ -14,7 +14,7 @@ import { get } from 'lodash'; const createResolversCacheMiddleware = ( registry, reducerKey ) => () => ( next ) => ( action ) => { const resolvers = registry.select( 'core/data' ).getCachedResolvers( reducerKey ); Object.entries( resolvers ).forEach( ( [ selectorName, resolversByArgs ] ) => { - const resolver = get( registry.namespaces, [ reducerKey, 'resolvers', selectorName ] ); + const resolver = get( registry.stores, [ reducerKey, 'resolvers', selectorName ] ); if ( ! resolver || ! resolver.shouldInvalidate ) { return; } diff --git a/packages/data/src/store/index.js b/packages/data/src/store/index.js index 67ff3b67220856..9a14aac264508e 100644 --- a/packages/data/src/store/index.js +++ b/packages/data/src/store/index.js @@ -1,12 +1,48 @@ -/** - * Internal dependencies - */ -import reducer from './reducer'; -import * as selectors from './selectors'; -import * as actions from './actions'; - -export default { - reducer, - actions, - selectors, -}; + +function createCoreDataStore( registry ) { + const getCoreDataSelector = ( selectorName ) => ( reducerKey, ...args ) => { + return registry.select( reducerKey )[ selectorName ]( ...args ); + }; + + const getCoreDataAction = ( actionName ) => ( reducerKey, ...args ) => { + return registry.dispatch( reducerKey )[ actionName ]( ...args ); + }; + + return { + getSelectors() { + return [ + 'getIsResolving', + 'hasStartedResolution', + 'hasFinishedResolution', + 'isResolving', + 'getCachedResolvers', + ].reduce( ( memo, selectorName ) => ( { + ...memo, + [ selectorName ]: getCoreDataSelector( selectorName ), + } ), {} ); + }, + + getActions() { + return [ + 'startResolution', + 'finishResolution', + 'invalidateResolution', + 'invalidateResolutionForStore', + 'invalidateResolutionForStoreSelector', + ].reduce( ( memo, actionName ) => ( { + ...memo, + [ actionName ]: getCoreDataAction( actionName ), + } ), {} ); + }, + + subscribe() { + // There's no reasons to trigger any listener when we subscribe to this store + // because there's no state stored in this store that need to retrigger selectors + // if a change happens, the corresponding store where the tracking stated live + // would have already triggered a "subscribe" call. + return () => {}; + }, + }; +} + +export default createCoreDataStore; diff --git a/test/unit/__mocks__/@wordpress/data.js b/test/unit/__mocks__/@wordpress/data.js deleted file mode 100644 index 729e53648b211e..00000000000000 --- a/test/unit/__mocks__/@wordpress/data.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Internal dependencies - */ -import { use, plugins } from '../../../../packages/data/src'; - -use( plugins.controls ); - -export * from '../../../../packages/data/src'; From c7f846a047463550e3bb09e414bbaadd20356c9f Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 28 Mar 2019 09:34:38 +0100 Subject: [PATCH 04/38] Data Module: Support parent/child registries (#14369) --- packages/data/README.md | 1 + packages/data/src/registry.js | 21 ++++++++++--- packages/data/src/test/registry.js | 48 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/packages/data/README.md b/packages/data/README.md index 66bdc2faa5b420..c496d0ef6dbf0a 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -297,6 +297,7 @@ configurations. _Parameters_ - _storeConfigs_ `Object`: Initial store configurations. +- _parent_ `?Object`: Parent registry. _Returns_ diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index e1c324f6ef8ac0..15f02180c7b859 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -34,11 +34,12 @@ import createCoreDataStore from './store'; * Creates a new store registry, given an optional object of initial store * configurations. * - * @param {Object} storeConfigs Initial store configurations. + * @param {Object} storeConfigs Initial store configurations. + * @param {Object?} parent Parent registry. * * @return {WPDataRegistry} Data registry. */ -export function createRegistry( storeConfigs = {} ) { +export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; let listeners = []; @@ -74,7 +75,11 @@ export function createRegistry( storeConfigs = {} ) { */ function select( reducerKey ) { const store = stores[ reducerKey ]; - return store && store.getSelectors(); + if ( store ) { + return store.getSelectors(); + } + + return parent && parent.select( reducerKey ); } /** @@ -87,7 +92,11 @@ export function createRegistry( storeConfigs = {} ) { */ function dispatch( reducerKey ) { const store = stores[ reducerKey ]; - return store && store.getActions(); + if ( store ) { + return store.getActions(); + } + + return parent && parent.dispatch( reducerKey ); } // @@ -172,5 +181,9 @@ export function createRegistry( storeConfigs = {} ) { ( [ name, config ] ) => registry.registerStore( name, config ) ); + if ( parent ) { + parent.subscribe( globalListener ); + } + return withPlugins( registry ); } diff --git a/packages/data/src/test/registry.js b/packages/data/src/test/registry.js index cd18ac2d892854..b828cd01991596 100644 --- a/packages/data/src/test/registry.js +++ b/packages/data/src/test/registry.js @@ -604,4 +604,52 @@ describe( 'createRegistry', () => { expect( registry.select() ).toBe( 10 ); } ); } ); + + describe( 'parent registry', () => { + it( 'should call parent registry selectors/actions if defined', () => { + const mySelector = jest.fn(); + const myAction = jest.fn(); + const getSelectors = () => ( { mySelector } ); + const getActions = () => ( { myAction } ); + const subscribe = () => {}; + registry.registerGenericStore( 'store', { getSelectors, getActions, subscribe } ); + const subRegistry = createRegistry( {}, registry ); + + subRegistry.select( 'store' ).mySelector(); + subRegistry.dispatch( 'store' ).myAction(); + + expect( mySelector ).toHaveBeenCalled(); + expect( myAction ).toHaveBeenCalled(); + } ); + + it( 'should override existing store in parent registry', () => { + const mySelector = jest.fn(); + const myAction = jest.fn(); + const getSelectors = () => ( { mySelector } ); + const getActions = () => ( { myAction } ); + const subscribe = () => {}; + registry.registerGenericStore( 'store', { getSelectors, getActions, subscribe } ); + + const subRegistry = createRegistry( {}, registry ); + const mySelector2 = jest.fn(); + const myAction2 = jest.fn(); + const getSelectors2 = () => ( { mySelector: mySelector2 } ); + const getActions2 = () => ( { myAction: myAction2 } ); + const subscribe2 = () => {}; + subRegistry.registerGenericStore( 'store', { + getSelectors: getSelectors2, + getActions: getActions2, + subscribe: subscribe2, + } ); + + subRegistry.select( 'store' ).mySelector(); + subRegistry.dispatch( 'store' ).myAction(); + + expect( mySelector ).not.toHaveBeenCalled(); + expect( myAction ).not.toHaveBeenCalled(); + + expect( mySelector2 ).toHaveBeenCalled(); + expect( myAction2 ).toHaveBeenCalled(); + } ); + } ); } ); From e4108fc3147be225d7bb9f7bee4ea909bf514552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Thu, 28 Mar 2019 10:33:25 +0100 Subject: [PATCH 05/38] Input Interaction: better horizontal edge detection (#14462) * Input Interaction: better horizontal edge detection * Correct BR ranges * Add e2e test * Increase buffer for Firefox * Clean up * Merge isEdge logic * Fix typo * Address feedback * Build docs --- packages/dom/README.md | 2 +- packages/dom/src/dom.js | 177 ++++++++---------- .../__snapshots__/writing-flow.test.js.snap | 10 + packages/e2e-tests/specs/writing-flow.test.js | 12 ++ 4 files changed, 97 insertions(+), 104 deletions(-) diff --git a/packages/dom/README.md b/packages/dom/README.md index 8a2186890d5e49..1212dea1f4f5d5 100644 --- a/packages/dom/README.md +++ b/packages/dom/README.md @@ -148,7 +148,7 @@ _Parameters_ _Returns_ -- `boolean`: True if at the edge, false if not. +- `boolean`: True if at the vertical edge, false if not. # **placeCaretAtHorizontalEdge** diff --git a/packages/dom/src/dom.js b/packages/dom/src/dom.js index b3b0f7b6df89da..87be698a3be8d0 100644 --- a/packages/dom/src/dom.js +++ b/packages/dom/src/dom.js @@ -60,14 +60,17 @@ function isSelectionForward( selection ) { } /** - * Check whether the selection is horizontally at the edge of the container. + * Check whether the selection is at the edge of the container. Checks for + * horizontal position by default. Set `onlyVertical` to true to check only + * vertically. * - * @param {Element} container Focusable element. - * @param {boolean} isReverse Set to true to check left, false for right. + * @param {Element} container Focusable element. + * @param {boolean} isReverse Set to true to check left, false to check right. + * @param {boolean} onlyVertical Set to true to check only vertical position. * - * @return {boolean} True if at the horizontal edge, false if not. + * @return {boolean} True if at the edge, false if not. */ -export function isHorizontalEdge( container, isReverse ) { +function isEdge( container, isReverse, onlyVertical ) { if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) { if ( container.selectionStart !== container.selectionEnd ) { return false; @@ -86,105 +89,16 @@ export function isHorizontalEdge( container, isReverse ) { const selection = window.getSelection(); - // Create copy of range for setting selection to find effective offset. - const range = selection.getRangeAt( 0 ).cloneRange(); - - // Collapse in direction of selection. - if ( ! selection.isCollapsed ) { - range.collapse( ! isSelectionForward( selection ) ); - } - - let node = range.startContainer; - - let extentOffset; - if ( isReverse ) { - // When in reverse, range node should be first. - extentOffset = 0; - } else if ( node.nodeValue ) { - // Otherwise, vary by node type. A text node has no children. Its range - // offset reflects its position in nodeValue. - // - // "If the startContainer is a Node of type Text, Comment, or - // CDATASection, then the offset is the number of characters from the - // start of the startContainer to the boundary point of the Range." - // - // See: https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset - // See: https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeValue - extentOffset = node.nodeValue.length; - } else { - // "For other Node types, the startOffset is the number of child nodes - // between the start of the startContainer and the boundary point of - // the Range." - // - // See: https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset - extentOffset = node.childNodes.length; - } - - // Offset of range should be at expected extent. - const position = isReverse ? 'start' : 'end'; - const offset = range[ `${ position }Offset` ]; - if ( offset !== extentOffset ) { + if ( ! selection.rangeCount ) { return false; } - // If confirmed to be at extent, traverse up through DOM, verifying that - // the node is at first or last child for reverse or forward respectively - // (ignoring empty text nodes). Continue until container is reached. - const order = isReverse ? 'previous' : 'next'; - - while ( node !== container ) { - let next = node[ `${ order }Sibling` ]; - - // Skip over empty text nodes. - while ( next && next.nodeType === TEXT_NODE && next.data === '' ) { - next = next[ `${ order }Sibling` ]; - } - - if ( next ) { - return false; - } - - node = node.parentNode; - } - - // If reached, range is assumed to be at edge. - return true; -} - -/** - * Check whether the selection 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. - * - * @return {boolean} True if at the edge, false if not. - */ -export function isVerticalEdge( container, isReverse ) { - if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) { - return isHorizontalEdge( container, isReverse ); - } - - if ( ! container.isContentEditable ) { - return true; - } - - const selection = window.getSelection(); - const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null; - - if ( ! range ) { - return false; - } - - const rangeRect = getRectangleFromRange( range ); + const rangeRect = getRectangleFromRange( selection.getRangeAt( 0 ) ); if ( ! rangeRect ) { return false; } - // Calculate a buffer that is half the line height. In some browsers, the - // selection rectangle may not fill the entire height of the line, so we add - // half the line height to the selection rectangle to ensure that it is well - // over its line boundary. const computedStyle = window.getComputedStyle( container ); const lineHeight = parseInt( computedStyle.lineHeight, 10 ); @@ -198,20 +112,65 @@ export function isVerticalEdge( container, isReverse ) { return false; } - const editableRect = container.getBoundingClientRect(); - const buffer = lineHeight / 2; + // Calculate a buffer that is half the line height. In some browsers, the + // selection rectangle may not fill the entire height of the line, so we add + // 3/4 the line height to the selection rectangle to ensure that it is well + // over its line boundary. + const buffer = 3 * parseInt( lineHeight, 10 ) / 4; + const containerRect = container.getBoundingClientRect(); + const verticalEdge = isReverse ? + containerRect.top > rangeRect.top - buffer : + containerRect.bottom < rangeRect.bottom + buffer; - // Too low. - if ( isReverse && rangeRect.top - buffer > editableRect.top ) { + if ( ! verticalEdge ) { return false; } - // Too high. - if ( ! isReverse && rangeRect.bottom + buffer < editableRect.bottom ) { + if ( onlyVertical ) { + return true; + } + + // To calculate the horizontal position, we insert a test range and see if + // this test range has the same horizontal position. This method proves to + // be better than a DOM-based calculation, because it ignores empty text + // nodes and a trailing line break element. In other words, we need to check + // visual positioning, not DOM positioning. + const x = isReverse ? containerRect.left + 1 : containerRect.right - 1; + const y = isReverse ? containerRect.top + buffer : containerRect.bottom - buffer; + const testRange = hiddenCaretRangeFromPoint( document, x, y, container ); + + if ( ! testRange ) { return false; } - return true; + const side = isReverse ? 'left' : 'right'; + const testRect = getRectangleFromRange( testRange ); + + return Math.round( testRect[ side ] ) === Math.round( rangeRect[ side ] ); +} + +/** + * Check whether the selection 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. + * + * @return {boolean} True if at the horizontal edge, false if not. + */ +export function isHorizontalEdge( container, isReverse ) { + return isEdge( container, isReverse ); +} + +/** + * Check whether the selection 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. + * + * @return {boolean} True if at the vertical edge, false if not. + */ +export function isVerticalEdge( container, isReverse ) { + return isEdge( container, isReverse, true ); } /** @@ -229,6 +188,18 @@ export function getRectangleFromRange( range ) { return range.getBoundingClientRect(); } + const { startContainer } = range; + + // Correct invalid "BR" ranges. The cannot contain any children. + if ( startContainer.nodeName === 'BR' ) { + const { parentNode } = startContainer; + const index = Array.from( parentNode.childNodes ).indexOf( startContainer ); + + range = document.createRange(); + range.setStart( parentNode, index ); + range.setEnd( parentNode, index ); + } + let rect = range.getClientRects()[ 0 ]; // If the collapsed range starts (and therefore ends) at an element node, diff --git a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap index 41f6fe04a2b531..2a80f20633a953 100644 --- a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap +++ b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap @@ -126,6 +126,16 @@ exports[`adding blocks should navigate around nested inline boundaries 2`] = ` " `; +exports[`adding blocks should navigate empty paragraph 1`] = ` +" +

1

+ + + +

2

+" +`; + exports[`adding blocks should not create extra line breaks in multiline value 1`] = ` "

diff --git a/packages/e2e-tests/specs/writing-flow.test.js b/packages/e2e-tests/specs/writing-flow.test.js index 62ece158016405..c186507a7cdef3 100644 --- a/packages/e2e-tests/specs/writing-flow.test.js +++ b/packages/e2e-tests/specs/writing-flow.test.js @@ -298,4 +298,16 @@ describe( 'adding blocks', () => { // Check that none of the paragraph blocks have
in them. expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should navigate empty paragraph', async () => { + await clickBlockAppender(); + await page.keyboard.press( 'Enter' ); + await page.waitForFunction( () => document.activeElement.isContentEditable ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.type( '1' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( '2' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); From bde17b64a48e9a7ad6c08a733747c35e54cc0fd5 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 28 Mar 2019 12:03:54 +0000 Subject: [PATCH 06/38] Code quality: Remove unnecessary BaseControl usage from video block (#14179) ## Description This PR applies a simple change to remove unnecessary BaseControl usage in the video block. It makes the video block code compliant with the lint rule being added on https://github.com/WordPress/gutenberg/pull/14151. I tried a different approach use the BaseControl as a label for the button being rendered inside it, but in my tests with a screen reader the button text stops being used and BaseControl label starts getting used, in this case, this change does not make sense, the button text should be used. ## How has this been tested? I checked that the Poster Image buttons still work. I checked that no visual changes happen. --- packages/block-library/src/video/edit.js | 4 +- packages/components/CHANGELOG.md | 3 +- .../components/src/base-control/README.md | 41 +++++++++++++++++++ packages/components/src/base-control/index.js | 11 ++++- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/video/edit.js b/packages/block-library/src/video/edit.js index 5b54e8ae0f0a4e..175d95871ecbb4 100644 --- a/packages/block-library/src/video/edit.js +++ b/packages/block-library/src/video/edit.js @@ -219,8 +219,10 @@ class VideoEdit extends Component { + + { __( 'Poster Image' ) } + ( + + + Author + + + +); +``` + +### Props + +#### className + +The class that will be added with `components-base-control__label` to the classes of the wrapper div. +If no className is passed only `components-base-control__label` is used. + +- Type: `String` +- Required: No + +#### children + +The content to be displayed within the `BaseControl.VisualLabel`. + +- Type: `Element` +- Required: Yes diff --git a/packages/components/src/base-control/index.js b/packages/components/src/base-control/index.js index 925914b7824154..37a5939e10775c 100644 --- a/packages/components/src/base-control/index.js +++ b/packages/components/src/base-control/index.js @@ -8,7 +8,7 @@ function BaseControl( { id, label, help, className, children } ) {
{ label && id && } - { label && ! id && { label } } + { label && ! id && { label } } { children }
{ !! help &&

{ help }

} @@ -16,4 +16,13 @@ function BaseControl( { id, label, help, className, children } ) { ); } +BaseControl.VisualLabel = ( { className, children } ) => { + className = classnames( 'components-base-control__label', className ); + return ( + + { children } + + ); +}; + export default BaseControl; From 1e40b281d1db725e512e383b5e00f3b59e333d5a Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Thu, 28 Mar 2019 13:55:17 +0100 Subject: [PATCH 07/38] Update the alt text description (#14668) * Update the alt text description. * Make the links non-translatable. * Update help prop documentation. --- packages/block-library/src/image/edit.js | 10 +++++++++- packages/block-library/src/media-text/edit.js | 10 +++++++++- packages/components/src/base-control/README.md | 2 +- packages/components/src/checkbox-control/README.md | 6 +++--- packages/components/src/radio-control/README.md | 4 ++-- packages/components/src/range-control/README.md | 4 ++-- packages/components/src/select-control/README.md | 8 ++++---- packages/components/src/text-control/README.md | 4 ++-- packages/components/src/textarea-control/README.md | 14 +++++++------- packages/components/src/toggle-control/README.md | 7 +++---- 10 files changed, 42 insertions(+), 27 deletions(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 51d2587591eda3..c6336b03508364 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -28,6 +28,7 @@ import { ToggleControl, Toolbar, withNotices, + ExternalLink, } from '@wordpress/components'; import { compose } from '@wordpress/compose'; import { withSelect } from '@wordpress/data'; @@ -456,7 +457,14 @@ class ImageEdit extends Component { label={ __( 'Alt Text (Alternative Text)' ) } value={ alt } onChange={ this.updateAlt } - help={ __( 'Alternative text describes your image to people who can’t see it. Add a short description with its key details.' ) } + help={ + + + { __( 'Describe the purpose of the image' ) } + + { __( 'Leave empty if the image is purely decorative.' ) } + + } /> { ! isEmpty( imageSizeOptions ) && ( + + { __( 'Describe the purpose of the image' ) } + + { __( 'Leave empty if the image is purely decorative.' ) } + + } /> ) } ); diff --git a/packages/components/src/base-control/README.md b/packages/components/src/base-control/README.md index 4a0dd8712ee3f3..f451a75b6ee4fd 100644 --- a/packages/components/src/base-control/README.md +++ b/packages/components/src/base-control/README.md @@ -45,7 +45,7 @@ If this property is added, a label will be generated using label property as the If this property is added, a help text will be generated using help property as the content. -- Type: `String` +- Type: `String|WPElement` - Required: No ### className diff --git a/packages/components/src/checkbox-control/README.md b/packages/components/src/checkbox-control/README.md index 91b50a09b2d970..c011fd2c56fe1f 100644 --- a/packages/components/src/checkbox-control/README.md +++ b/packages/components/src/checkbox-control/README.md @@ -60,7 +60,7 @@ import { withState } from '@wordpress/compose'; const MyCheckboxControl = withState( { isChecked: true, -} )( ( { isChecked, setState } ) => ( +} )( ( { isChecked, setState } ) => ( ( +} )( ( { option, setState } ) => ( ( +} )( ( { columns, setState } ) => ( ( + } )( ( { size, setState } ) => ( ( +} )( ( { className, setState } ) => ( ( + } )( ( { text, setState } ) => ( ( +} )( ( { hasFixedBackground, setState } ) => ( setState( ( state ) => ( { hasFixedBackground: ! state.hasFixedBackground } ) ) } /> @@ -37,7 +37,7 @@ If this property is added, a label will be generated using label property as the If this property is added, a help text will be generated using help property as the content. -- Type: `String` | `Function` +- Type: `String|WPElement` - Required: No ### checked @@ -61,4 +61,3 @@ The class that will be added with `components-base-control` and `components-togg Type: String Required: No - From a66fa9b15cf3a957fcd1be0341239de209d0b176 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 28 Mar 2019 12:59:42 +0000 Subject: [PATCH 08/38] Add documentation for MediaPlaceholder props. (#14645) --- .../components/media-placeholder/README.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/packages/block-editor/src/components/media-placeholder/README.md b/packages/block-editor/src/components/media-placeholder/README.md index 2890d2f1f162a1..9dc983ab7a8d8c 100644 --- a/packages/block-editor/src/components/media-placeholder/README.md +++ b/packages/block-editor/src/components/media-placeholder/README.md @@ -27,6 +27,92 @@ const { MediaPlaceholder } = wp.editor; } ``` +## Props + +### accept + +A string passed to `FormFileUpload` that tells the browser which file types can be upload to the upload window the browser use e.g: `image/*,video/*`. +More information about this string is available in https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers. +This property is similar to the `allowedTypes` property. The difference is the format and the fact that this property affects the behavior of `FormFileUpload` while `allowedTypes` affects the behavior `MediaUpload`. + +- Type: `String` +- Required: No + +### addToGallery + +If true, and if `gallery === true` the gallery media modal opens directly in the media library where the user can add additional images. When uploading/selecting files on the placeholder, the placeholder appends the files to the existing files list. +If false the gallery media modal opens in the edit mode where the user can edit existing images, by reordering them, remove them, or change their attributes. When uploading/selecting files on the placeholder the files replace the existing files list. + +- Type: `Boolean` +- Required: No +- Default: `false` + +### allowedTypes + +Array with the types of the media to upload/select from the media library. +Each type is a string that can contain the general mime type e.g: `image`, `audio`, `text`, +or the complete mime type e.g: `audio/mpeg`, `image/gif`. +If allowedTypes is unset all mime types should be allowed. +This property is similar to the `accept` property. The difference is the format and the fact that this property affects the behavior of `MediaUpload` while `accept` affects the behavior `FormFileUpload`. + +- Type: `Array` +- Required: No + +### className + +Class name added to the placeholder. + +- Type: `String` +- Required: No + +### isAppender + +If true, the property changes the look of the placeholder to be adequate to scenarios where new files are added to an already existing set of files, e.g., adding files to a gallery. +If false the default placeholder style is used. + +- Type: `Boolean` +- Required: No +- Default: `false` + +### labels + +An object that can contain a `title` and `instructions` properties. These properties are passed to the placeholder component as `label` and `instructions` respectively. + +- Type: `Object` +- Required: No + + +### multiple + +Whether to allow multiple selection of files or not. + +- Type: `Boolean` +- Required: No +- Default: `false` + +### onError + +Callback called when an upload error happens. + +- Type: `Function` +- Required: No + +### onSelect + +Callback called when the files are selected/uploaded. +The call back receives an array with the new files. Each element of the collection is an object containing the media properties of the file e.g.: `url`, `id`,... + +- Type: `Function` +- Required: Yes + +### 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 + + ## Extend It includes a `wp.hooks` filter `editor.MediaPlaceholder` that enables developers to replace or extend it. From a3ef292ff60fb108de8bc0e8b15f84c232ab2170 Mon Sep 17 00:00:00 2001 From: Kjell Reigstad Date: Thu, 28 Mar 2019 09:01:46 -0400 Subject: [PATCH 09/38] Remove negative toolbar position rules from full-aligned blocks. (#14669) --- packages/block-editor/src/components/block-list/style.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss index b1566d2ae03caa..03ed0f07bd5de3 100644 --- a/packages/block-editor/src/components/block-list/style.scss +++ b/packages/block-editor/src/components/block-list/style.scss @@ -904,6 +904,12 @@ .block-editor-block-contextual-toolbar > * { pointer-events: auto; } + + // Full-aligned blocks have negative margins on the parent of the toolbar, so additional position adjustment is not required. + &[data-align="full"] .block-editor-block-contextual-toolbar { + left: 0; + right: 0; + } } .block-editor-block-list__block.is-focus-mode:not(.is-multi-selected) > .block-editor-block-contextual-toolbar { From 406baf149ff5fa26f865c99e14ff7bc3f488dc29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= Date: Thu, 28 Mar 2019 14:22:20 +0100 Subject: [PATCH 10/38] Blocks API: Improve validation after block gets filters applied (#14529) * Block API: Improve validation after block gets filters applied * Update CHANGELOG.md * Update CHANGELOG.md * Apply suggestions from code review Co-Authored-By: gziolo * Update CHANGELOG.md --- packages/blocks/CHANGELOG.md | 6 ++++++ packages/blocks/src/api/registration.js | 17 ++++++++++++++--- packages/blocks/src/api/test/registration.js | 17 +++++++++++++---- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 96d8125c985e19..55def7da9db4ce 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,3 +1,9 @@ +## x.x.x (Unreleased) + +### New Feature + +- Added a default implementation for `save` setting in `registerBlockType` which saves no markup in the post content. + ## 6.1.0 (2019-03-06) ### New Feature diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 727ee171f2b799..f6d5a29544803c 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -3,7 +3,12 @@ /** * External dependencies */ -import { get, isFunction, some } from 'lodash'; +import { + get, + isFunction, + isPlainObject, + some, +} from 'lodash'; /** * WordPress dependencies @@ -91,10 +96,16 @@ export function registerBlockType( name, settings ) { } settings = applyFilters( 'blocks.registerBlockType', settings, name ); + if ( ! isPlainObject( settings ) ) { + console.error( + 'Block settings must be a valid object.' + ); + return; + } - if ( ! settings || ! isFunction( settings.save ) ) { + if ( ! isFunction( settings.save ) ) { console.error( - 'The "save" property must be specified and must be a valid function.' + 'The "save" property must be a valid function.' ); return; } diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 7b47b97afb8408..05cc3140779eaa 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -3,7 +3,7 @@ /** * External dependencies */ -import { noop, omit } from 'lodash'; +import { noop } from 'lodash'; /** * WordPress dependencies @@ -106,6 +106,15 @@ describe( 'blocks', () => { expect( block ).toBeUndefined(); } ); + it( 'should reject blocks with invalid save function', () => { + const block = registerBlockType( 'my-plugin/fancy-block-5', { + ...defaultBlockSettings, + save: 'invalid', + } ); + expect( console ).toHaveErroredWith( 'The "save" property must be a valid function.' ); + expect( block ).toBeUndefined(); + } ); + it( 'should reject blocks with an invalid edit function', () => { const blockType = { save: noop, edit: 'not-a-function', category: 'common', title: 'block title' }, block = registerBlockType( 'my-plugin/fancy-block-6', blockType ); @@ -318,12 +327,12 @@ describe( 'blocks', () => { expect( block ).toBeUndefined(); } ); - it( 'should reject valid blocks when they become invalid after executing filter which removes save property', () => { + it( 'should reject blocks which become invalid after executing filter which does not return a plain object', () => { addFilter( 'blocks.registerBlockType', 'core/blocks/without-save', ( settings ) => { - return omit( settings, 'save' ); + return [ settings ]; } ); const block = registerBlockType( 'my-plugin/fancy-block-13', defaultBlockSettings ); - expect( console ).toHaveErroredWith( 'The "save" property must be specified and must be a valid function.' ); + expect( console ).toHaveErroredWith( 'Block settings must be a valid object.' ); expect( block ).toBeUndefined(); } ); } ); From cd2da7d118f19b911833e3b0ad2a4376eb5087d4 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak Date: Thu, 28 Mar 2019 06:29:06 -0700 Subject: [PATCH 11/38] Update Blocks Tutorial to match Gutenberg Examples (#14584) * Add link to JS Build documentation * Update step 01 to match examples repo * Update step 02 to match examples repo * Update step 03 to match examples repo * Update step 04 to match examples repo * Apply suggestions from code review Co-Authored-By: mkaz * Update docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md Co-Authored-By: mkaz * Add plugin header * Per review, wrap in IIFE, matches examples * Per review, add description for editor.css file * Fix action callback * Add style.css code and reorganize a bit * Add note about changing the handle * Fix example * Fix dynamic block example * Fix up ESNext example * Fix live rendering section --- .../applying-styles-with-stylesheets.md | 134 +++++++------ .../block-controls-toolbars-and-inspector.md | 176 ++++++++--------- .../block-tutorial/creating-dynamic-blocks.md | 162 ++++++++------- ...roducing-attributes-and-editable-fields.md | 185 +++++++++--------- .../tutorials/block-tutorial/readme.md | 4 +- .../writing-your-first-block-type.md | 87 ++++---- 6 files changed, 398 insertions(+), 350 deletions(-) diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md b/docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md index 47d2f70876143e..dff770151e8e9c 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md @@ -1,48 +1,56 @@ # Applying Styles From a Stylesheet -In the previous section, the block had applied its own styles by an inline `style` attribute. While this might be adequate for very simple components, you will quickly find that it becomes easier to write your styles by extracting them to a separate stylesheet file. +In the previous step, the block had applied its own styles by an inline `style` attribute. While this might be adequate for very simple components, you will quickly find that it becomes easier to write your styles by extracting them to a separate stylesheet file. -The editor will automatically generate a class name for each block type to simplify styling. It can be accessed from the object argument passed to the edit and save functions: +The editor will automatically generate a class name for each block type to simplify styling. It can be accessed from the object argument passed to the edit and save functions. In step 2, we will create a stylesheet to use that class name. {% codetabs %} {% ES5 %} ```js -var el = wp.element.createElement, - registerBlockType = wp.blocks.registerBlockType; - -registerBlockType( 'gutenberg-boilerplate-es5/hello-world-step-02', { - title: 'Hello World (Step 2)', - - icon: 'universal-access-alt', - - category: 'layout', - - edit: function( props ) { - return el( 'p', { className: props.className }, 'Hello editor.' ); - }, - - save: function() { - return el( 'p', {}, 'Hello saved content.' ); - } -} ); +( function( blocks, element ) { + var el = element.createElement; + + blocks.registerBlockType( 'gutenberg-examples/example-02-stylesheets', { + title: 'Example: Stylesheets', + icon: 'universal-access-alt', + category: 'layout', + edit: function( props ) { + return el( + 'p', + { className: props.className }, + 'Hello World, step 2 (from the editor, in green).' + ); + }, + save: function() { + return el( + 'p', + {}, + 'Hello World, step 2 (from the frontend, in red).' + ); + }, + } ); +}( + window.wp.blocks, + window.wp.element +) ); ``` {% ESNext %} ```js const { registerBlockType } = wp.blocks; -registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-02', { - title: 'Hello World (Step 2)', +registerBlockType( 'gutenberg-examples/example-02-stylesheets', { + title: 'Example: Stylesheets', icon: 'universal-access-alt', category: 'layout', edit( { className } ) { - return

Hello editor.

; + return

Hello World, step 2 (from the editor, in green).

; }, save() { - return

Hello saved content.

; + return

Hello World, step 2 (from the frontend, in red)./p>; } } ); ``` @@ -50,8 +58,16 @@ registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-02', { The class name is generated using the block's name prefixed with `wp-block-`, replacing the `/` namespace separator with a single `-`. +## Enqueueing Editor and Front end Assets + +Like scripts, you need to enqueue your block's styles. As explained in the section before, you use the `editor_style` handle for styles only relevant in the editor, and the `style` handle for common styles applied both in the editor and the front of your site. + +The stylesheets enqueued by `style` are the base styles and are loaded first. The `editor` stylesheet will be loaded after it. + +Let's move on into code. Create a file called `editor.css`: + ```css -.wp-block-gutenberg-boilerplate-es5-hello-world-step-02 { +.wp-block-gutenberg-examples-example-02-stylesheets { color: green; background: #cfc; border: 2px solid #9c9; @@ -59,56 +75,52 @@ The class name is generated using the block's name prefixed with `wp-block-`, re } ``` -## Enqueueing Editor-only Block Assets +And a new `style.css` file containing: -Like scripts, your block's editor-specific styles should be enqueued by assigning the `editor_styles` setting of the registered block type: +```css +.wp-block-gutenberg-examples-example-02-stylesheets { + color: darkred; + background: #fcc; + border: 2px solid #c99; + padding: 20px; +} +``` + +Configure your plugin to use these new styles: ```php 'gutenberg-boilerplate-es5-step02-editor', - 'editor_style' => 'gutenberg-boilerplate-es5-step02-editor', - ) ); -} -add_action( 'init', 'gutenberg_boilerplate_block' ); -``` - -## Enqueueing Editor and Front end Assets - -While a block's scripts are usually only necessary to load in the editor, you'll want to load styles both on the front of your site and in the editor. You may even want distinct styles in each context. - -When registering a block, you can assign one or both of `style` and `editor_style` to respectively assign styles always loaded for a block or styles only loaded in the editor. - -```php - 'gutenberg-boilerplate-es5-step02', + register_block_type( 'gutenberg-examples/example-02-stylesheets', array( + 'style' => 'gutenberg-examples-02', + 'editor_style' => 'gutenberg-examples-02-editor', + 'editor_script' => 'gutenberg-examples-02', ) ); } -add_action( 'init', 'gutenberg_boilerplate_block' ); +add_action( 'init', 'gutenberg_examples_02_register_block' ); ``` - -Since your block is likely to share some styles in both contexts, you can consider `style.css` as the base stylesheet, placing editor-specific styles in `editor.css`. diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md b/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md index b2efe54ef4d1ac..bfabb7379b40a6 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md @@ -13,50 +13,45 @@ You can also customize the toolbar to include controls specific to your block ty {% codetabs %} {% ES5 %} ```js -var el = wp.element.createElement, - Fragment = wp.element.Fragment - registerBlockType = wp.blocks.registerBlockType, - RichText = wp.editor.RichText, - BlockControls = wp.editor.BlockControls, - AlignmentToolbar = wp.editor.AlignmentToolbar; - -registerBlockType( 'gutenberg-boilerplate-es5/hello-world-step-04', { - title: 'Hello World (Step 4)', - - icon: 'universal-access-alt', - - category: 'layout', - - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p', +( function( blocks, editor, element ) { + var el = element.createElement; + var RichText = editor.RichText; + var AlignmentToolbar = editor.AlignmentToolbar; + var BlockControls = editor.BlockControls; + + blocks.registerBlockType( 'gutenberg-examples/example-04-controls', { + title: 'Example: Controls', + icon: 'universal-access-alt', + category: 'layout', + + attributes: { + content: { + type: 'array', + source: 'children', + selector: 'p', + }, + alignment: { + type: 'string', + default: 'none', + }, }, - alignment: { - type: 'string', - }, - }, - edit: function( props ) { - var content = props.attributes.content, - alignment = props.attributes.alignment; + edit: function( props ) { + var content = props.attributes.content; + var alignment = props.attributes.alignment; - function onChangeContent( newContent ) { - props.setAttributes( { content: newContent } ); - } + function onChangeContent( newContent ) { + props.setAttributes( { content: newContent } ); + } - function onChangeAlignment( newAlignment ) { - props.setAttributes( { alignment: newAlignment } ); - } + function onChangeAlignment( newAlignment ) { + props.setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } ); + } - return ( - el( - Fragment, - null, + return [ el( BlockControls, - null, + { key: 'controls' }, el( AlignmentToolbar, { @@ -68,98 +63,99 @@ registerBlockType( 'gutenberg-boilerplate-es5/hello-world-step-04', { el( RichText, { - key: 'editable', + key: 'richtext', tagName: 'p', - className: props.className, style: { textAlign: alignment }, + className: props.className, onChange: onChangeContent, value: content, } - ) - ) - ); - }, - - save: function( props ) { - var content = props.attributes.content, - alignment = props.attributes.alignment; + ), + ]; + }, - return el( RichText.Content, { - tagName: 'p', - className: props.className, - style: { textAlign: alignment }, - value: content - } ); - }, -} ); + save: function( props ) { + return el( RichText.Content, { + tagName: 'p', + className: 'gutenberg-examples-align-' + props.attributes.alignment, + value: props.attributes.content, + } ); + }, + } ); +}( + window.wp.blocks, + window.wp.editor, + window.wp.element +) ); ``` {% ESNext %} ```js const { registerBlockType } = wp.blocks; -const { Fragment } = wp.element; + const { RichText, - BlockControls, AlignmentToolbar, + BlockControls, } = wp.editor; -registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-04', { - title: 'Hello World (Step 4)', - +registerBlockType( 'gutenberg-examples/example-04-controls-esnext', { + title: 'Example: Controls (esnext)', icon: 'universal-access-alt', - category: 'layout', - attributes: { content: { - type: 'string', - source: 'html', + type: 'array', + source: 'children', selector: 'p', }, alignment: { type: 'string', + default: 'none', }, }, + edit: ( props ) => { + const { + attributes: { + content, + alignment, + }, + className, + } = props; + + const onChangeContent = ( newContent ) => { + props.setAttributes( { content: newContent } ); + }; - edit( { attributes, className, setAttributes } ) { - const { content, alignment } = attributes; - - function onChangeContent( newContent ) { - setAttributes( { content: newContent } ); - } - - function onChangeAlignment( newAlignment ) { - setAttributes( { alignment: newAlignment } ); - } + const onChangeAlignment = ( newAlignment ) => { + props.setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } ); + }; return ( - - - - +

+ { + + + + } - +
); }, - - save( { attributes } ) { - const { content, alignment } = attributes; - + save: ( props ) => { return ( ); }, diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md b/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md index ab7543c392c9cb..8c92d48349b7e8 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md @@ -1,56 +1,59 @@ # Creating dynamic blocks -It is possible to create dynamic blocks. These are blocks that can change their content even if the post is not saved. One example from WordPress itself is the latest posts block. This block will update everywhere it is used when a new post is published. +Dynamic blocks are blocks that can change their content even if the post is not saved. One example from WordPress itself is the latest posts block. This block will update everywhere it is used when a new post is published. -The following code example shows how to create the latest post block dynamic block. +The following code example shows how to create a dynamic block that shows only the last post as a link. {% codetabs %} {% ES5 %} ```js -// myblock.js +( function( blocks, element, data ) { -var el = wp.element.createElement, - registerBlockType = wp.blocks.registerBlockType, - withSelect = wp.data.withSelect; + var el = element.createElement, + registerBlockType = blocks.registerBlockType, + withSelect = data.withSelect; -registerBlockType( 'my-plugin/latest-post', { - title: 'Latest Post', - icon: 'megaphone', - category: 'widgets', - - edit: withSelect( function( select ) { - return { - posts: select( 'core' ).getEntityRecords( 'postType', 'post' ) - }; - } )( function( props ) { + registerBlockType( 'gutenberg-examples/example-05-dynamic', { + title: 'Example: last post', + icon: 'megaphone', + category: 'widgets', - if ( ! props.posts ) { - return "Loading..."; - } - - if ( props.posts.length === 0 ) { - return "No posts"; - } - var className = props.className; - var post = props.posts[ 0 ]; - - return el( - 'a', - { className: className, href: post.link }, - post.title.rendered - ); - } ), -} ); + edit: withSelect( function( select ) { + return { + posts: select( 'core' ).getEntityRecords( 'postType', 'post' ) + }; + } )( function( props ) { + + if ( ! props.posts ) { + return "Loading..."; + } + + if ( props.posts.length === 0 ) { + return "No posts"; + } + var className = props.className; + var post = props.posts[ 0 ]; + + return el( + 'a', + { className: className, href: post.link }, + post.title.rendered + ); + } ), + } ); +}( + window.wp.blocks, + window.wp.element, + window.wp.data, +) ); ``` {% ESNext %} ```js -// myblock.js - const { registerBlockType } = wp.blocks; const { withSelect } = wp.data; -registerBlockType( 'my-plugin/latest-post', { - title: 'Latest Post', +registerBlockType( 'gutenberg-examples/example-05-dynamic', { + title: 'Example: last post', icon: 'megaphone', category: 'widgets', @@ -78,13 +81,16 @@ registerBlockType( 'my-plugin/latest-post', { ``` {% end %} -Because it is a dynamic block it doesn't need to override the default `save` implementation on the client. Instead, it needs a server component. The rendering can be added using the `render_callback` property when using the `register_block_type` function. +Because it is a dynamic block it doesn't need to override the default `save` implementation on the client. Instead, it needs a server component. The contents in the front of your site depend on the function called by the `render_callback` property of `register_block_type`. ```php 1, 'post_status' => 'publish', @@ -101,9 +107,21 @@ function my_plugin_render_block_latest_post( $attributes, $content ) { ); } -register_block_type( 'my-plugin/latest-post', array( - 'render_callback' => 'my_plugin_render_block_latest_post', -) ); +function gutenberg_examples_05_dynamic() { + wp_register_script( + 'gutenberg-examples-05', + plugins_url( 'block.js', __FILE__ ), + array( 'wp-blocks', 'wp-element', 'wp-data' ) + ); + + register_block_type( 'gutenberg-examples/example-05-dynamic', array( + 'editor_script' => 'gutenberg-examples-05', + 'render_callback' => 'gutenberg_examples_05_dynamic_render_callback' + ) ); + +} +add_action( 'init', 'gutenberg_examples_05_dynamic' ); + ``` There are a few things to notice: @@ -121,45 +139,47 @@ Gutenberg 2.8 added the [``](/packages/components/src/server-s {% codetabs %} {% ES5 %} ```js -// myblock.js - -var el = wp.element.createElement, - registerBlockType = wp.blocks.registerBlockType, - ServerSideRender = wp.components.ServerSideRender; - -registerBlockType( 'my-plugin/latest-post', { - title: 'Latest Post', - icon: 'megaphone', - category: 'widgets', - - edit: function( props ) { - // ensure the block attributes matches this plugin's name - return ( - el(ServerSideRender, { - block: "my-plugin/latest-post", - attributes: props.attributes - }) - ); - }, -} ); +( function( blocks, element, components ) { + + var el = element.createElement, + registerBlockType = blocks.registerBlockType, + ServerSideRender = components.ServerSideRender; + + registerBlockType( 'gutenberg-examples/example-05-dynamic', { + title: 'Example: last post', + icon: 'megaphone', + category: 'widgets', + + edit: function( props ) { + + return ( + el(ServerSideRender, { + block: "gutenberg-examples/example-05-dynamic", + attributes: props.attributes + } ) + ); + }, + } ); +}( + window.wp.blocks, + window.wp.element, + window.wp.components, +) ); ``` {% ESNext %} ```js -// myblock.js - const { registerBlockType } = wp.blocks; const { ServerSideRender } = wp.components; -registerBlockType( 'my-plugin/latest-post', { - title: 'Latest Post', +registerBlockType( 'gutenberg-examples/example-05-dynamic', { + title: 'Example: last post', icon: 'megaphone', category: 'widgets', edit: function( props ) { - // ensure the block attributes matches this plugin's name return ( ); @@ -168,4 +188,4 @@ registerBlockType( 'my-plugin/latest-post', { ``` {% end %} -The PHP code is the same as above and is automatically handled through the WP REST API. +Note that this code uses the `wp.components` utility but not `wp.data`. Make sure to update the `wp-data` dependency to `wp-compononents` in the PHP code. diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md b/docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md index c51bd505bdd2e2..738ed6640ce55f 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md @@ -1,6 +1,6 @@ # Introducing Attributes and Editable Fields -Our example block is still not very interesting because it lacks options to customize the appearance of the message. In this section, we will implement a RichText field allowing the user to specify their own message. Before doing so, it's important to understand how the state of a block (its "attributes") is maintained and changed over time. +The example blocks so far are still not very interesting because they lack options to customize the appearance of the message. In this section, we will implement a RichText field allowing the user to specify their own message. Before doing so, it's important to understand how the state of a block (its "attributes") is maintained and changed over time. ## Attributes @@ -8,84 +8,121 @@ Until now, the `edit` and `save` functions have returned a simple representation One challenge of maintaining the representation of a block as a JavaScript object is that we must be able to extract this object again from the saved content of a post. This is achieved with the block type's `attributes` property: -{% codetabs %} -{% ES5 %} ```js -var el = wp.element.createElement, - registerBlockType = wp.blocks.registerBlockType, - RichText = wp.editor.RichText; - -registerBlockType( 'gutenberg-boilerplate-es5/hello-world-step-03', { - title: 'Hello World (Step 3)', - - icon: 'universal-access-alt', - - category: 'layout', - attributes: { content: { - type: 'string', - source: 'html', + type: 'array', + source: 'children', selector: 'p', - } + }, }, +``` - edit: function( props ) { - var content = props.attributes.content; +When registering a new block type, the `attributes` property describes the shape of the attributes object you'd like to receive in the `edit` and `save` functions. Each value is a [source function](/docs/designers-developers/developers/block-api/block-attributes.md) to find the desired value from the markup of the block. + +In the code snippet above, when loading the editor, the `content` value will be extracted from the HTML of the paragraph element in the saved post's markup. + +## Components and the `RichText` Component + +Earlier examples used the `createElement` function to create DOM nodes, but it's also possible to encapsulate this behavior into "components". This abstraction helps you share common behaviors and hide complexity in self-contained units. - function onChangeContent( newContent ) { - props.setAttributes( { content: newContent } ); - } +There are a number of [components available](/docs/designers-developers/developers/packages/packages-editor.md#components) to use in implementing your blocks. You can see one such component in the code below: the [`RichText` component](/docs/designers-developers/developers/packages/packages-editor.md#richtext). - return el( - RichText, - { - tagName: 'p', - className: props.className, - onChange: onChangeContent, - value: content, +The `RichText` component can be considered as a super-powered `textarea` element, enabling rich content editing including bold, italics, hyperlinks, etc. + +To use the `RichText` component, add `wp-editor` to the dependency array of registered script handles when calling `wp_register_script`. + +```php +wp_register_script( + 'gutenberg-examples-03', + plugins_url( 'block.js', __FILE__ ), + array( + 'wp-blocks', + 'wp-element', + 'wp-editor' // Note the addition of wp-editor to the dependencies + ), + filemtime( plugin_dir_path( __FILE__ ) . 'block.js' ) +); +``` + +Do not forget to also update the `editor_script` handle in `register_block_type` to `gutenberg-examples-03`. + +Implementing this behavior as a component enables you as the block implementer to be much more granular about editable fields. Your block may not need `RichText` at all, or it may need many independent `RichText` elements, each operating on a subset of the overall block state. + +Because `RichText` allows for nested nodes, you'll most often use it in conjunction with the `html` attribute source when extracting the value from saved content. You'll also use `RichText.Content` in the `save` function to output RichText values. + +Here is the complete block definition for Example 03. + +{% codetabs %} +{% ES5 %} +```js +( function( blocks, editor, element ) { + var el = element.createElement; + var RichText = editor.RichText; + + blocks.registerBlockType( 'gutenberg-examples/example-03-editable', { + title: 'Example: Editable', + icon: 'universal-access-alt', + category: 'layout', + + attributes: { + content: { + type: 'array', + source: 'children', + selector: 'p', + }, + }, + + edit: function( props ) { + var content = props.attributes.content; + function onChangeContent( newContent ) { + props.setAttributes( { content: newContent } ); } - ); - }, - save: function( props ) { - var content = props.attributes.content; + return el( + RichText, + { + tagName: 'p', + className: props.className, + onChange: onChangeContent, + value: content, + } + ); + }, - return el( RichText.Content, { - tagName: 'p', - className: props.className, - value: content - } ); - }, -} ); + save: function( props ) { + return el( RichText.Content, { + tagName: 'p', value: props.attributes.content, + } ); + }, + } ); +}( + window.wp.blocks, + window.wp.editor, + window.wp.element +) ); ``` {% ESNext %} ```js const { registerBlockType } = wp.blocks; const { RichText } = wp.editor; -registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-03', { - title: 'Hello World (Step 3)', - +registerBlockType( 'gutenberg-examples/example-03-editable-esnext', { + title: 'Example: Editable (esnext)', icon: 'universal-access-alt', - category: 'layout', - attributes: { content: { - type: 'string', - source: 'html', + type: 'array', + source: 'children', selector: 'p', }, }, - - edit( { attributes, className, setAttributes } ) { - const { content } = attributes; - - function onChangeContent( newContent ) { + edit: ( props ) => { + const { attributes: { content }, setAttributes, className } = props; + const onChangeContent = ( newContent ) => { setAttributes( { content: newContent } ); - } - + }; return ( ); }, - - save( { attributes } ) { - const { content } = attributes; - - return ( - - ); + save: ( props ) => { + return ; }, } ); ``` {% end %} - -When registering a new block type, the `attributes` property describes the shape of the attributes object you'd like to receive in the `edit` and `save` functions. Each value is a [source function](/docs/designers-developers/developers/block-api/block-attributes.md) to find the desired value from the markup of the block. - -In the code snippet above, when loading the editor, we will extract the `content` value as the HTML of the paragraph element in the saved post's markup. - -## Components and the `RichText` Component - -Earlier examples used the `createElement` function to create DOM nodes, but it's also possible to encapsulate this behavior into ["components"](). This abstraction helps as a pattern to share common behaviors and to hide complexity into self-contained units. There are a number of components available to use in implementing your blocks. You can see one such component in the snippet above: the [`RichText` component](). - -The `RichText` component can be considered as a super-powered `textarea` element, enabling rich content editing including bold, italics, hyperlinks, etc. - -To use the `RichText` component, add `wp-editor` to the array of registered script handles when calling `wp_register_script`. - -```php -wp_register_script( - 'gutenberg-boilerplate-es5-step03', - plugins_url( 'step-03/block.js', __FILE__ ), - array( - 'wp-blocks', - 'wp-element', - 'wp-editor', // Note the addition of wp-editor to the dependencies - ) -); -``` - -Implementing this behavior as a component enables you as the block implementer to be much more granular about editable fields. Your block may not need `RichText` at all, or it may need many independent `RichText` elements, each operating on a subset of the overall block state. - -Because `RichText` allows for nested nodes, you'll most often use it in conjunction with the `html` attribute source when extracting the value from saved content. You'll also use `RichText.Content` in the `save` function to output RichText values. diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/readme.md b/docs/designers-developers/developers/tutorials/block-tutorial/readme.md index 0ff9418d01743a..a3ee5191dbca79 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/readme.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/readme.md @@ -4,4 +4,6 @@ The purpose of this tutorial is to step through the fundamentals of creating a n To follow along with this tutorial, you can [download the accompanying WordPress plugin](https://github.com/WordPress/gutenberg-examples) which includes all of the examples for you to try on your own site. At each step along the way, you should feel free to experiment by modifying the examples with your own ideas and observing the effects they have on the block's behavior. -Code snippets are provided both for "classic" JavaScript (ECMAScript 5, or "ES5"), as well as newer versions of the language standard (ES2015 and newer, or "ESNext"). You can change between them using tabs found above each code example. When choosing to author your blocks with ESNext, you will need a build step in order to support older browsers. Note that it is not required to use ESNext to create a new block, and you are welcome to use classic JavaScript if you so choose. +Code snippets are provided both for "classic" JavaScript (ECMAScript 5, or "ES5"), as well as newer versions of the language standard (ES2015 and newer, or "ESNext"). You can change between them using tabs found above each code example. When choosing to author your blocks with ESNext, you need to run [the JavaScript build step](/docs/designers-developers/developers/tutorials/javascript/js-build-setup/) in order to support older browsers. + +Note that it is not required to use ESNext to create a new block, and you are welcome to use classic JavaScript if you so choose. diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md b/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md index 3dfb4645fcc3d6..c959b4cd5aec58 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md @@ -10,19 +10,22 @@ While the block's editor behaviors are implemented in JavaScript, you'll need to ```php 'gutenberg-boilerplate-es5-step01', + register_block_type( 'gutenberg-examples/example-01-basic', array( + 'editor_script' => 'gutenberg-examples-01', ) ); + } -add_action( 'init', 'gutenberg_boilerplate_block' ); +add_action( 'init', 'gutenberg_examples_01_register_block' ); ``` Note the two script dependencies: @@ -39,44 +42,58 @@ With the script enqueued, let's look at the implementation of the block itself: {% codetabs %} {% ES5 %} ```js -var el = wp.element.createElement, - registerBlockType = wp.blocks.registerBlockType, - blockStyle = { backgroundColor: '#900', color: '#fff', padding: '20px' }; - -registerBlockType( 'gutenberg-boilerplate-es5/hello-world-step-01', { - title: 'Hello World (Step 1)', - - icon: 'universal-access-alt', - - category: 'layout', - - edit: function() { - return el( 'p', { style: blockStyle }, 'Hello editor.' ); - }, - - save: function() { - return el( 'p', { style: blockStyle }, 'Hello saved content.' ); - }, -} ); +( function( blocks, element ) { + var el = element.createElement; + + var blockStyle = { + backgroundColor: '#900', + color: '#fff', + padding: '20px', + }; + + blocks.registerBlockType( 'gutenberg-examples/example-01-basic', { + title: 'Example: Basic', + icon: 'universal-access-alt', + category: 'layout', + edit: function() { + return el( + 'p', + { style: blockStyle }, + 'Hello World, step 1 (from the editor).' + ); + }, + save: function() { + return el( + 'p', + { style: blockStyle }, + 'Hello World, step 1 (from the frontend).' + ); + }, + } ); +}( + window.wp.blocks, + window.wp.element +) ); ``` {% ESNext %} ```js const { registerBlockType } = wp.blocks; -const blockStyle = { backgroundColor: '#900', color: '#fff', padding: '20px' }; -registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-01', { - title: 'Hello World (Step 1)', +const blockStyle = { + backgroundColor: '#900', + color: '#fff', + padding: '20px', +}; +registerBlockType( 'gutenberg-examples/example-01-basic-esnext', { + title: 'Example: Basic (esnext)', icon: 'universal-access-alt', - category: 'layout', - edit() { - return

Hello editor.

; + return
Basic example with JSX! (editor)
; }, - save() { - return

Hello saved content.

; + return
Basic example with JSX! (front)
; }, } ); ``` @@ -86,6 +103,6 @@ _By now you should be able to see `Hello editor` in the admin side and `Hello sa Once a block is registered, you should immediately see that it becomes available as an option in the editor inserter dialog, using values from `title`, `icon`, and `category` to organize its display. You can choose an icon from any included in the built-in [Dashicons icon set](https://developer.wordpress.org/resource/dashicons/), or provide a [custom svg element](/docs/designers-developers/developers/block-api/block-registration.md#icon-optional). -A block name must be prefixed with a namespace specific to your plugin. This helps prevent conflicts when more than one plugin registers a block with the same name. +A block name must be prefixed with a namespace specific to your plugin. This helps prevent conflicts when more than one plugin registers a block with the same name. In this example, the namespace is `gutenberg-examples`. The `edit` and `save` functions describe the structure of your block in the context of the editor and the saved content respectively. While the difference is not obvious in this simple example, in the following sections we'll explore how these are used to enable customization of the block in the editor preview. From b629aef60b26e33d4f561faf2bf104d2f4c2963b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Thu, 28 Mar 2019 15:16:00 +0100 Subject: [PATCH 12/38] Add @ellatrix to CODEOWNERS file (#14682) --- .github/CODEOWNERS | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2de6c549969961..b4001b7eda58c7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,12 +13,12 @@ /packages/block-library @youknowriad @gziolo @Soean @ajitbohra @jorgefilipecosta @talldan @noisysocks @notnownikki # Editor -/packages/annotations @youknowriad @aduth @atimmer +/packages/annotations @youknowriad @aduth @atimmer @ellatrix /packages/autop @youknowriad @aduth -/packages/block-editor @youknowriad @gziolo @talldan @noisysocks +/packages/block-editor @youknowriad @gziolo @talldan @noisysocks @ellatrix /packages/block-serialization-spec-parser @youknowriad @gziolo @aduth @dmsnell /packages/block-serialization-default-parser @youknowriad @gziolo @aduth @dmsnell -/packages/blocks @youknowriad @gziolo @aduth @noisysocks +/packages/blocks @youknowriad @gziolo @aduth @noisysocks @ellatrix /packages/edit-post @youknowriad @talldan @noisysocks /packages/editor @youknowriad @talldan @noisysocks /packages/list-reusable-blocks @youknowriad @aduth @noisysocks @@ -29,7 +29,7 @@ # Tooling /bin @youknowriad @gziolo @aduth @ntwb @nerrad @ajitbohra -/docs/tool @youknowriad @gziolo @chrisvanpatten @mkaz @ajitbohra @nosolosw @notnownikki +/docs/tool @youknowriad @gziolo @chrisvanpatten @mkaz @ajitbohra @nosolosw @notnownikki /packages/babel-plugin-import-jsx-pragma @youknowriad @gziolo @aduth @ntwb @nerrad @ajitbohra @nosolosw /packages/babel-plugin-makepot @youknowriad @gziolo @aduth @ntwb @nerrad @ajitbohra /packages/babel-preset-default @youknowriad @gziolo @aduth @ntwb @nerrad @ajitbohra @nosolosw @@ -60,13 +60,13 @@ /packages/blob @youknowriad @gziolo @aduth /packages/date @youknowriad @gziolo @aduth /packages/deprecated @youknowriad @gziolo @aduth -/packages/dom @youknowriad @gziolo @aduth @nosolosw +/packages/dom @youknowriad @gziolo @aduth @nosolosw @ellatrix /packages/dom-ready @youknowriad @gziolo @aduth /packages/escape-html @youknowriad @gziolo @aduth /packages/html-entities @youknowriad @gziolo @aduth /packages/i18n @youknowriad @aduth @swissspidy /packages/is-shallow-equal @youknowriad @gziolo @aduth -/packages/keycodes @youknowriad @gziolo @aduth @talldan +/packages/keycodes @youknowriad @gziolo @aduth @talldan @ellatrix /packages/priority-queue @youknowriad @gziolo @aduth /packages/token-list @youknowriad @gziolo @aduth /packages/url @youknowriad @gziolo @aduth @talldan @@ -77,9 +77,9 @@ /packages/plugins @youknowriad @gziolo @aduth @adamsilverstein # Rich Text -/packages/format-library @youknowriad @aduth @iseulde @jorgefilipecosta -/packages/rich-text @youknowriad @aduth @iseulde @jorgefilipecosta -/packages/block-editor/src/components/rich-text @youknowriad @aduth @iseulde @jorgefilipecosta +/packages/format-library @youknowriad @aduth @ellatrix @jorgefilipecosta +/packages/rich-text @youknowriad @aduth @ellatrix @jorgefilipecosta +/packages/block-editor/src/components/rich-text @youknowriad @aduth @ellatrix @jorgefilipecosta # PHP /lib @youknowriad @gziolo @aduth From 4c600b01ae695dc40120e03f519f8f7fb0742a31 Mon Sep 17 00:00:00 2001 From: Marco Fernandes Date: Thu, 28 Mar 2019 14:58:21 +0000 Subject: [PATCH 13/38] Fix typo on build script. (#14686) --- packages/scripts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 16b22d3d7bfee9..0c1687ed49f9c2 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -23,7 +23,7 @@ _Example:_ ```json { "scripts": { - "build": "wp-scripts run build", + "build": "wp-scripts build", "check-engines": "wp-scripts check-engines", "check-licenses": "wp-scripts check-licenses --production", "lint:css": "wp-scripts lint-style '**/*.css'", From 91b0c41e11719e6b985f61cdefe640dcdb805d49 Mon Sep 17 00:00:00 2001 From: Kjell Reigstad Date: Thu, 28 Mar 2019 13:17:42 -0400 Subject: [PATCH 14/38] Restore proper radio button appearance on desktop screens. (#14684) PR #14624 mistakenly used negative pixel values for the position of the dot inside of radio buttons. This PR restores the correct values, to ensure proper placement. --- assets/stylesheets/_mixins.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/stylesheets/_mixins.scss b/assets/stylesheets/_mixins.scss index 89297e6335bc92..0d602338bd8727 100644 --- a/assets/stylesheets/_mixins.scss +++ b/assets/stylesheets/_mixins.scss @@ -484,7 +484,7 @@ background-color: $white; @include break-medium() { - margin: -3px 0 0 -3px; + margin: 3px 0 0 3px; } } } From 95277f63ac243f4d66cf5993a5fa8b527069d895 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak Date: Thu, 28 Mar 2019 14:42:19 -0700 Subject: [PATCH 15/38] Fix minor typos in inline docs (#14690) --- .../src/components/post-taxonomies/flat-term-selector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/components/post-taxonomies/flat-term-selector.js b/packages/editor/src/components/post-taxonomies/flat-term-selector.js index 233f07cc3e2aa0..3b4c1c8a0c2926 100644 --- a/packages/editor/src/components/post-taxonomies/flat-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/flat-term-selector.js @@ -38,7 +38,7 @@ const isSameTermName = ( termA, termB ) => termA.toLowerCase() === termB.toLower /** * Returns a term object with name unescaped. - * The unescape of the name propery is done using lodash unescape function. + * The unescape of the name property is done using lodash unescape function. * * @param {Object} term The term object to unescape. * @@ -57,7 +57,7 @@ const unescapeTerm = ( term ) => { * * @param {Object[]} terms Array of term objects to unescape. * - * @return {Object[]} Array of therm objects unscaped. + * @return {Object[]} Array of term objects unescaped. */ const unescapeTerms = ( terms ) => { return map( terms, unescapeTerm ); From fa7549ef4236c60ee413be1446f0e69927f59c73 Mon Sep 17 00:00:00 2001 From: Joen Asmussen Date: Fri, 29 Mar 2019 05:55:52 +0100 Subject: [PATCH 16/38] Fix issue with double scrollbar in Fullscreen Mode (#14677) This PR fixes an issue where the sidebar would have two scrollbars when in fullscreen mode. --- packages/edit-post/src/components/sidebar/style.scss | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/edit-post/src/components/sidebar/style.scss b/packages/edit-post/src/components/sidebar/style.scss index 793088a0390911..62936b0f0a7ff6 100644 --- a/packages/edit-post/src/components/sidebar/style.scss +++ b/packages/edit-post/src/components/sidebar/style.scss @@ -40,17 +40,10 @@ z-index: z-index(".edit-post-sidebar .components-panel"); @include break-small() { - overflow: inherit; + overflow: hidden; height: auto; max-height: none; } - - @include break-medium() { - - body.is-fullscreen-mode & { - max-height: calc(100vh - #{ $panel-header-height }); - } - } } > .components-panel .components-panel__header { From 74310114ebdb15848e4b2394e783f7ed7e13a74e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 29 Mar 2019 09:08:30 +0100 Subject: [PATCH 17/38] Automatically use a subregistry when using the block editor provider (#14678) --- .../src/components/provider/index.js | 9 +++- .../provider/with-registry-provider.js | 41 +++++++++++++++++++ packages/block-editor/src/store/index.js | 6 ++- .../editor/src/components/provider/index.js | 1 + 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 packages/block-editor/src/components/provider/with-registry-provider.js diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index fd788477a0d6d1..f321b19dc1de8d 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -3,9 +3,14 @@ */ import { Component } from '@wordpress/element'; import { DropZoneProvider, SlotFillProvider } from '@wordpress/components'; -import { withDispatch, withRegistry } from '@wordpress/data'; +import { withDispatch } from '@wordpress/data'; import { compose } from '@wordpress/compose'; +/** + * Internal dependencies + */ +import withRegistryProvider from './with-registry-provider'; + class BlockEditorProvider extends Component { componentDidMount() { this.props.updateSettings( this.props.settings ); @@ -115,6 +120,7 @@ class BlockEditorProvider extends Component { } export default compose( [ + withRegistryProvider, withDispatch( ( dispatch ) => { const { updateSettings, @@ -126,5 +132,4 @@ export default compose( [ resetBlocks, }; } ), - withRegistry, ] )( BlockEditorProvider ); diff --git a/packages/block-editor/src/components/provider/with-registry-provider.js b/packages/block-editor/src/components/provider/with-registry-provider.js new file mode 100644 index 00000000000000..cdb3b7fe7b2ded --- /dev/null +++ b/packages/block-editor/src/components/provider/with-registry-provider.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { useState, useEffect } from '@wordpress/element'; +import { withRegistry, createRegistry, RegistryProvider } from '@wordpress/data'; +import { createHigherOrderComponent } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { storeConfig } from '../../store'; +import applyMiddlewares from '../../store/middlewares'; + +const withRegistryProvider = createHigherOrderComponent( ( WrappedComponent ) => { + return withRegistry( ( { useSubRegistry = true, registry, ...props } ) => { + if ( ! useSubRegistry ) { + return ; + } + + const [ subRegistry, setSubRegistry ] = useState( null ); + useEffect( () => { + const newRegistry = createRegistry( {}, registry ); + const store = newRegistry.registerStore( 'core/block-editor', storeConfig ); + // This should be removed after the refactoring of the effects to controls. + applyMiddlewares( store ); + setSubRegistry( newRegistry ); + }, [ registry ] ); + + if ( ! subRegistry ) { + return null; + } + + return ( + + + + ); + } ); +}, 'withRegistryProvider' ); + +export default withRegistryProvider; diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index 0119e63d7a3d16..485238f46f606d 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -17,11 +17,15 @@ import controls from './controls'; */ const MODULE_KEY = 'core/block-editor'; -const store = registerStore( MODULE_KEY, { +export const storeConfig = { reducer, selectors, actions, controls, +}; + +const store = registerStore( MODULE_KEY, { + ...storeConfig, persist: [ 'preferences' ], } ); applyMiddlewares( store ); diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index ca1cbe02626865..54405f61247b8f 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -154,6 +154,7 @@ class EditorProvider extends Component { onInput={ resetEditorBlocksWithoutUndoLevel } onChange={ resetEditorBlocks } settings={ editorSettings } + useSubRegistry={ false } > { children } From ae3a09edb85fce6fd36ae01e88351cc35bd2121a Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 29 Mar 2019 05:15:58 -0400 Subject: [PATCH 18/38] Data: Avoid assuming persisted preferences shape (#14692) --- packages/block-editor/src/store/selectors.js | 2 +- .../block-editor/src/store/test/selectors.js | 8 +++++--- packages/edit-post/src/store/selectors.js | 7 +++++-- .../edit-post/src/store/test/selectors.js | 18 +++++++++++++++++ packages/nux/src/store/selectors.js | 4 ++-- packages/nux/src/store/test/selectors.js | 20 +++++++++++++++++++ 6 files changed, 51 insertions(+), 8 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 03f02965457099..fb431d1a48d0f6 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1115,7 +1115,7 @@ export const canInsertBlockType = createSelector( * the number of inserts that have occurred. */ function getInsertUsage( state, id ) { - return state.preferences.insertUsage[ id ] || null; + return get( state.preferences.insertUsage, [ id ], null ); } /** diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index fde8a74fefa91f..3bbe1abd7f3a85 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -1927,9 +1927,11 @@ describe( 'selectors', () => { { id: 1, isTemporary: false, clientId: 'block1', title: 'Reusable Block 1' }, ], }, - preferences: { - insertUsage: {}, - }, + // Intentionally include a test case which considers + // `insertUsage` as not present within preferences. + // + // See: https://github.com/WordPress/gutenberg/issues/14580 + preferences: {}, blockListSettings: {}, }; const items = getInserterItems( state ); diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index c77a3add99e4b7..2430ee41087232 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -139,7 +139,10 @@ export function isEditorPanelEnabled( state, panelName ) { */ export function isEditorPanelOpened( state, panelName ) { const panels = getPreference( state, 'panels' ); - return panels[ panelName ] === true || get( panels, [ panelName, 'opened' ], false ); + return ( + get( panels, [ panelName ] ) === true || + get( panels, [ panelName, 'opened' ] ) === true + ); } /** @@ -163,7 +166,7 @@ export function isModalActive( state, modalName ) { * @return {boolean} Is active. */ export function isFeatureActive( state, feature ) { - return !! state.preferences.features[ feature ]; + return get( state.preferences.features, [ feature ], false ); } /** diff --git a/packages/edit-post/src/store/test/selectors.js b/packages/edit-post/src/store/test/selectors.js index b035169b3044d8..16131bf8eaf8d9 100644 --- a/packages/edit-post/src/store/test/selectors.js +++ b/packages/edit-post/src/store/test/selectors.js @@ -273,6 +273,15 @@ describe( 'selectors', () => { } ); describe( 'isEditorPanelOpened', () => { + it( 'is tolerant to an undefined panels preference', () => { + // See: https://github.com/WordPress/gutenberg/issues/14580 + const state = { + preferences: {}, + }; + + expect( isEditorPanelOpened( state, 'post-status' ) ).toBe( false ); + } ); + it( 'should return false by default', () => { const state = { preferences: { @@ -333,6 +342,15 @@ describe( 'selectors', () => { } ); describe( 'isFeatureActive', () => { + it( 'is tolerant to an undefined features preference', () => { + // See: https://github.com/WordPress/gutenberg/issues/14580 + const state = { + preferences: {}, + }; + + expect( isFeatureActive( state, 'chicken' ) ).toBe( false ); + } ); + it( 'should return true if feature is active', () => { const state = { preferences: { diff --git a/packages/nux/src/store/selectors.js b/packages/nux/src/store/selectors.js index 71669709d42cb2..9225bd97077eee 100644 --- a/packages/nux/src/store/selectors.js +++ b/packages/nux/src/store/selectors.js @@ -2,7 +2,7 @@ * External dependencies */ import createSelector from 'rememo'; -import { includes, difference, keys } from 'lodash'; +import { includes, difference, keys, has } from 'lodash'; /** * An object containing information about a guide. @@ -55,7 +55,7 @@ export function isTipVisible( state, tipId ) { return false; } - if ( state.preferences.dismissedTips[ tipId ] ) { + if ( has( state.preferences.dismissedTips, [ tipId ] ) ) { return false; } diff --git a/packages/nux/src/store/test/selectors.js b/packages/nux/src/store/test/selectors.js index ed63a7fa828de5..546d052958dd5e 100644 --- a/packages/nux/src/store/test/selectors.js +++ b/packages/nux/src/store/test/selectors.js @@ -53,6 +53,26 @@ describe( 'selectors', () => { } ); describe( 'isTipVisible', () => { + it( 'is tolerant to individual preferences being undefined', () => { + // See: https://github.com/WordPress/gutenberg/issues/14580 + const state = { + guides: [], + preferences: {}, + }; + expect( isTipVisible( state, 'test/tip' ) ).toBe( false ); + } ); + + it( 'is tolerant to undefined dismissedTips', () => { + // See: https://github.com/WordPress/gutenberg/issues/14580 + const state = { + guides: [], + preferences: { + areTipsEnabled: true, + }, + }; + expect( isTipVisible( state, 'test/tip' ) ).toBe( true ); + } ); + it( 'should return true by default', () => { const state = { guides: [], From 4b503acbd939220939e7cea295e5b8072e7d3909 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 29 Mar 2019 06:12:40 -0400 Subject: [PATCH 19/38] Data: Avoid unsetting insertUsage preference in block editor migration (#14691) * Data: Avoid unsetting insertUsage preference in block editor migration * Update index.js --- .../data/src/plugins/persistence/index.js | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/data/src/plugins/persistence/index.js b/packages/data/src/plugins/persistence/index.js index 4c9f5c7c0f2b97..a21fcb9b442fe2 100644 --- a/packages/data/src/plugins/persistence/index.js +++ b/packages/data/src/plugins/persistence/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { merge, isPlainObject, omit } from 'lodash'; +import { merge, isPlainObject, get } from 'lodash'; /** * Internal dependencies @@ -72,7 +72,7 @@ export function createPersistenceInterface( options ) { * * @return {Object} Persisted data. */ - function get() { + function getData() { if ( data === undefined ) { // If unset, getItem is expected to return null. Fall back to // empty object. @@ -99,12 +99,15 @@ export function createPersistenceInterface( options ) { * @param {string} key Key to update. * @param {*} value Updated value. */ - function set( key, value ) { + function setData( key, value ) { data = { ...data, [ key ]: value }; storage.setItem( storageKey, JSON.stringify( data ) ); } - return { get, set }; + return { + get: getData, + set: setData, + }; } /** @@ -209,20 +212,18 @@ persistencePlugin.__unstableMigrate = ( pluginOptions ) => { const persistence = createPersistenceInterface( pluginOptions ); // Preferences migration to introduce the block editor module - const persistedState = persistence.get(); - const coreEditorState = persistedState[ 'core/editor' ]; - if ( coreEditorState && coreEditorState.preferences && coreEditorState.preferences.insertUsage ) { - const blockEditorState = { + const insertUsage = get( persistence.get(), [ + 'core/editor', + 'preferences', + 'insertUsage', + ] ); + + if ( insertUsage ) { + persistence.set( 'core/block-editor', { preferences: { - insertUsage: coreEditorState.preferences.insertUsage, + insertUsage, }, - }; - - persistence.set( 'core/editor', { - ...coreEditorState, - preferences: omit( coreEditorState.preferences, [ 'insertUsage' ] ), } ); - persistence.set( 'core/block-editor', blockEditorState ); } }; From 3214cb4d795a6938a9d471cfb2ccc59297dfaff9 Mon Sep 17 00:00:00 2001 From: Nicola Heald Date: Fri, 29 Mar 2019 10:30:50 +0000 Subject: [PATCH 20/38] Fix WordPress embed block resolution (#14658) --- packages/block-library/src/embed/util.js | 2 +- packages/e2e-tests/specs/embedding.test.js | 25 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index 085ea4b22ff8c8..9cba9f50364745 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -46,7 +46,7 @@ export const findBlock = ( url ) => { }; export const isFromWordPress = ( html ) => { - return includes( html, 'class="wp-embedded-content" data-secret' ); + return includes( html, 'class="wp-embedded-content"' ); }; export const getPhotoHtml = ( photo ) => { diff --git a/packages/e2e-tests/specs/embedding.test.js b/packages/e2e-tests/specs/embedding.test.js index 3ea51358b14b21..4433531316109e 100644 --- a/packages/e2e-tests/specs/embedding.test.js +++ b/packages/e2e-tests/specs/embedding.test.js @@ -9,6 +9,8 @@ import { createJSONResponse, getEditedPostContent, clickButton, + insertBlock, + publishPost, } from '@wordpress/e2e-test-utils'; const MOCK_EMBED_WORDPRESS_SUCCESS_RESPONSE = { @@ -192,4 +194,27 @@ describe( 'Embedding content', () => { await clickButton( 'Try again' ); await page.waitForSelector( 'figure.wp-block-embed-twitter' ); } ); + + it( 'should switch to the WordPress block correctly', async () => { + // This test is to make sure that WordPress embeds are detected correctly, + // because the HTML can vary, and the block is detected by looking for + // classes in the HTML, so we need to flag up if the HTML changes. + + // Publish a post to embed. + await insertBlock( 'Paragraph' ); + await page.keyboard.type( 'Hello there!' ); + await publishPost(); + const postUrl = await page.$eval( '#inspector-text-control-0', ( el ) => el.value ); + + // Start a new post, embed the previous post. + await createNewPost(); + await clickBlockAppender(); + await page.keyboard.type( '/embed' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( postUrl ); + await page.keyboard.press( 'Enter' ); + + // Check the block has become a WordPress block. + await page.waitForSelector( '.wp-block-embed-wordpress' ); + } ); } ); From fd7289225bd51db05d5d8505d144de7ad5166481 Mon Sep 17 00:00:00 2001 From: Nicola Heald Date: Fri, 29 Mar 2019 15:45:24 +0000 Subject: [PATCH 21/38] Retry failing embeds with trailing slash (#14705) * Fix embedding Twitter URLs with a trailing slash (Closes #12664) * Fix race condition for WordPress URLs that end in slashes, add test --- packages/block-library/src/embed/edit.js | 13 ++++++++++++- packages/e2e-tests/specs/embedding.test.js | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index 0e251a8f0aafd0..fcc305d9e8a7c3 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -61,13 +61,24 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { if ( switchedPreview || switchedURL ) { if ( this.props.cannotEmbed ) { - // Can't embed this URL, and we've just received or switched the preview. + // We either have a new preview or a new URL, but we can't embed it. + if ( ! this.props.fetching ) { + // If we're not fetching the preview, then we know it can't be embedded, so try + // removing any trailing slash, and resubmit. + this.resubmitWithoutTrailingSlash(); + } return; } this.handleIncomingPreview(); } } + resubmitWithoutTrailingSlash() { + this.setState( ( prevState ) => ( { + url: prevState.url.replace( /\/$/, '' ), + } ), this.setUrl ); + } + setUrl( event ) { if ( event ) { event.preventDefault(); diff --git a/packages/e2e-tests/specs/embedding.test.js b/packages/e2e-tests/specs/embedding.test.js index 4433531316109e..6fb26c02281c70 100644 --- a/packages/e2e-tests/specs/embedding.test.js +++ b/packages/e2e-tests/specs/embedding.test.js @@ -62,6 +62,10 @@ const MOCK_BAD_WORDPRESS_RESPONSE = { }; const MOCK_RESPONSES = [ + { + match: createEmbeddingMatcher( 'https://wordpress.org/gutenberg/handbook' ), + onRequestMatch: createJSONResponse( MOCK_BAD_WORDPRESS_RESPONSE ), + }, { match: createEmbeddingMatcher( 'https://wordpress.org/gutenberg/handbook/' ), onRequestMatch: createJSONResponse( MOCK_BAD_WORDPRESS_RESPONSE ), @@ -82,6 +86,10 @@ const MOCK_RESPONSES = [ match: createEmbeddingMatcher( 'https://twitter.com/notnownikki' ), onRequestMatch: createJSONResponse( MOCK_EMBED_RICH_SUCCESS_RESPONSE ), }, + { + match: createEmbeddingMatcher( 'https://twitter.com/notnownikki/' ), + onRequestMatch: createJSONResponse( MOCK_CANT_EMBED_RESPONSE ), + }, { match: createEmbeddingMatcher( 'https://twitter.com/thatbunty' ), onRequestMatch: createJSONResponse( MOCK_BAD_EMBED_PROVIDER_RESPONSE ), @@ -175,6 +183,17 @@ describe( 'Embedding content', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + it( 'should retry embeds that could not be embedded with trailing slashes, without the trailing slashes', async () => { + await clickBlockAppender(); + await page.keyboard.type( '/embed' ); + await page.keyboard.press( 'Enter' ); + // This URL can't be embedded, but without the trailing slash, it can. + await page.keyboard.type( 'https://twitter.com/notnownikki/' ); + await page.keyboard.press( 'Enter' ); + // The twitter block should appear correctly. + await page.waitForSelector( 'figure.wp-block-embed-twitter' ); + } ); + it( 'should allow the user to try embedding a failed URL again', async () => { // URL that can't be embedded. await clickBlockAppender(); From 39171ca106b288b6ae1af06563a013a52b84ed2b Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 29 Mar 2019 15:55:50 +0000 Subject: [PATCH 22/38] Make tests resilient against transforms added by plugins (#14632) --- .../e2e-tests/specs/block-transforms.test.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/e2e-tests/specs/block-transforms.test.js b/packages/e2e-tests/specs/block-transforms.test.js index 7eae4155ab22fb..d76f546ebc9849 100644 --- a/packages/e2e-tests/specs/block-transforms.test.js +++ b/packages/e2e-tests/specs/block-transforms.test.js @@ -95,7 +95,13 @@ const getTransformResult = async ( blockContent, transformName ) => { return getEditedPostContent(); }; -describe( 'Block transforms', () => { +// Skipping all the tests when plugins are enabled +// makes sure the tests are not executed, and no unused snapshots errors are thrown. +const maybeDescribe = process.env.POPULAR_PLUGINS ? + describe : + describe.skip; + +maybeDescribe( 'Block transforms', () => { // Todo: Remove the filter as soon as all fixtures are corrected, // and its direct usage on the editor does not trigger errors. // Currently some fixtures trigger errors (mainly media related) @@ -166,10 +172,10 @@ describe( 'Block transforms', () => { ( { originalBlock, availableTransforms }, fixture ) => ( map( availableTransforms, - ( distinationBlock ) => ( [ + ( destinationBlock ) => ( [ originalBlock, fixture, - distinationBlock, + destinationBlock, ] ) ) ) @@ -177,10 +183,10 @@ describe( 'Block transforms', () => { it.each( testTable )( 'block %s in fixture %s into the %s block', - async ( originalBlock, fixture, distinationBlock ) => { + async ( originalBlock, fixture, destinationBlock ) => { const { content } = transformStructure[ fixture ]; expect( - await getTransformResult( content, distinationBlock ) + await getTransformResult( content, destinationBlock ) ).toMatchSnapshot(); } ); From edf6b37fd887d2eb31c5427a42625ce54092f23c Mon Sep 17 00:00:00 2001 From: Aaron Jorbin Date: Fri, 29 Mar 2019 12:55:29 -0400 Subject: [PATCH 23/38] Allow failures on php versions below 5.5 (#14541) This is a follow up to https://core.trac.wordpress.org/changeset/44950 --- .travis.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.travis.yml b/.travis.yml index 26b72a571f723d..e1a21e3f72cc5c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -99,3 +99,15 @@ jobs: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests - npm run build - npm run test-e2e -- --ci --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 2 == 1' < ~/.jest-e2e-tests ) + allow_failures: + - name: PHP unit tests (PHP 5.3) + env: WP_VERSION=latest SWITCH_TO_PHP=5.3 + script: + - ./bin/run-wp-unit-tests.sh + if: branch = master and type != "pull_request" + + - name: PHP unit tests (PHP 5.2) + env: WP_VERSION=latest SWITCH_TO_PHP=5.2 + script: + - ./bin/run-wp-unit-tests.sh + From 445b39eb005e694a5e1b9c9c5b41280cac2f2bd5 Mon Sep 17 00:00:00 2001 From: Dave Whitley Date: Fri, 29 Mar 2019 14:04:18 -0500 Subject: [PATCH 24/38] Adding design documentation to the Notice readme (#14514) * Adding design documentation to the Notice readme * Updating copy based on feedback --- packages/components/src/notice/README.md | 81 +++++++++++++++++++++++- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/packages/components/src/notice/README.md b/packages/components/src/notice/README.md index 9b0b62937b7f84..16de133c4346ae 100644 --- a/packages/components/src/notice/README.md +++ b/packages/components/src/notice/README.md @@ -1,8 +1,79 @@ # Notice -This component is used to display notices in editor. +Use Notices to communicate prominent messages to the user. -## Usage +![Notice component](https://make.wordpress.org/design/files/2019/03/Notice-Screenshot-alt.png) + +## Table of contents + +1. [Design guidelines](#design-guidelines) +2. [Development guidelines](#development-guidelines) +3. [Related components](#related-components) + +## Design guidelines + +A Notice displays a succinct message. It can also offer the user options, like viewing a published post or updating a setting, and requires a user action to be dismissed. + +Use Notices to communicate things that are important but don’t necessarily require action — a user can keep using the product even if they don’t choose to act on a Notice. They are less interruptive than a Modal. + +### Anatomy + +![Diagram of a Notice component with numbered labels](https://make.wordpress.org/design/files/2019/03/Notice-Anatomy.png) + +1. Container (status indicated with color) +2. Icon (optional) +3. Message +4. Dismiss icon (optional) + +### Usage + +Notices display at the top of the screen, below any toolbars anchored to the top of the page. They’re persistent and non-modal. Since they don’t overlay the content, users can ignore or dismiss them, and choose when to interact with them. + +![](https://make.wordpress.org/design/files/2019/03/Notice-States.png) + +Notices are color-coded to indicate the type of message being communicated: + +- **Default** notices have **no background**. +- **Informational** notices are **blue.** +- **Success** notices are **green.** +- **Warning** notices are **yellow****.** +- **Error** notices are **red.** + +If an icon is included in the Notice, it should be color-coded to match the Notice state. + +![A success Notice for updating a post](https://make.wordpress.org/design/files/2019/03/Notice-Do-1-alt.png) + +**Do** +Do use a Notice when you want to communicate a message of medium importance. + +![A Notice that requires an immediate action](https://make.wordpress.org/design/files/2019/03/Notice-Dont-1-alt.png) + +**Don’t** +Don't use a Notice for a message that requires immediate attention and action from the user. Use a Modal for this instead. + +![A success Notice for publishing a post](https://make.wordpress.org/design/files/2019/03/Notice-Do-2-alt.png) + +**Do** +Do display Notices at the top of the screen, below any toolbars. + +![A success Notice on top of the editor toolbar](https://make.wordpress.org/design/files/2019/03/Notice-Dont-2-alt.png) + +**Don’t** +Don't show Notices on top of toolbars. + +![An error Notice using red](https://make.wordpress.org/design/files/2019/03/Notice-Do-3-alt.png) + +**Do** +Do use color to indicate the type of message being communicated. + +![An error Notice using purple](https://make.wordpress.org/design/files/2019/03/Notice-Dont-3-alt.png) + +**Don’t** +Don't apply any colors other than those for Warnings, Success, or Errors. + +## Development guidelines + +### Usage To display a plain notice, pass `Notice` a string: @@ -24,7 +95,7 @@ const MyNotice = () => ( ); ``` -### Props +#### Props The following props are used to control the display of the component. @@ -32,3 +103,7 @@ The following props are used to control the display of the component. * `onRemove`: function called when dismissing the notice * `isDismissible`: (boolean) defaults to true, whether the notice should be dismissible or not * `actions`: (array) an array of action objects. Each member object should contain a `label` and either a `url` link string or `onClick` callback function. A `className` property can be used to add custom classes to the button styles. By default, some classes are used (e.g: is-link or is-default) the default classes can be removed by setting property `noDefaultClasses` to `false`. + +## Related components + +- To create a more prominent message that requires action, use a Modal. From 580fedd587962304f21964a64778298be2f88511 Mon Sep 17 00:00:00 2001 From: Dave Whitley Date: Fri, 29 Mar 2019 14:53:49 -0500 Subject: [PATCH 25/38] Update TextControl design documentation (#14710) * Update TextControl design documentation * Removing `http://` from anchor links. * Re-arrange Do/Don't image labels To align with the formatting of other READMEs. * Re-edit Do/Don't labels for better consistency. --- .../components/src/text-control/README.md | 104 +++++++++++++----- 1 file changed, 74 insertions(+), 30 deletions(-) diff --git a/packages/components/src/text-control/README.md b/packages/components/src/text-control/README.md index 6c1c214a27f029..49066ad3cf86ba 100644 --- a/packages/components/src/text-control/README.md +++ b/packages/components/src/text-control/README.md @@ -1,72 +1,116 @@ # TextControl +TextControl components let users enter and edit text. -TextControl is normally used to generate text input fields. But can be used to generate other input types. +![Unfilled and filled TextControl components](https://make.wordpress.org/design/files/2019/03/TextControl.png) +## Table of contents -## Usage +1. [Design guidelines](#design-guidelines) +2. [Development guidelines](#development-guidelines) +3. [Related components](#related-components) -Render a user interface to input the name of an additional css class. +## Design guidelines + +### Usage + +#### When to use TextControls + +TextControls are best used for free text entry. If you have a set of predefined options you want users to select from, it’s best to use a more constrained component, such as a SelectControl, RadioControl, CheckboxControl, or RangeControl. + +Because TextControls are single-line fields, they are not suitable for collecting long responses. For those, use a text area instead. + +TextControls should: + +- Stand out and indicate that users can input information. +- Have clearly differentiated states (selected/unselected, active/inactive). +- Make it easy to understand the requested information and to address any errors. +- Have visible labels; placeholder text is not an acceptable replacement for a label as it vanishes when users start typing. + +### Anatomy + +![Features of a TextControl component with numbered labels](https://make.wordpress.org/design/files/2019/03/TextControl-Anatomy.png) + +1. Label +2. Input container +3. Input text + +#### Label text +Label text is used to inform users as to what information is requested for a text field. Every text field should have a label. Label text should be above the input field, and always visible. + +#### Containers +Containers improve the discoverability of text fields by creating contrast between the text field and surrounding content. + +![A TextControl with a stroke around the container to clearly indicate the input area](https://make.wordpress.org/design/files/2019/03/TextControl-Do.png) -```jsx -import { TextControl } from '@wordpress/components'; -import { withState } from '@wordpress/compose'; +**Do** +A stroke around the container clearly indicates that users can input information. -const MyTextControl = withState( { - className: '', -} )( ( { className, setState } ) => ( - setState( { className } ) } - /> -) ); -``` +![A TextControl without a clear visual marker to indicate the input area](https://make.wordpress.org/design/files/2019/03/TextControl-Dont.png) -## Props +**Don’t** +Don’t use unclear visual markers to indicate a text field. + +## Development guidelines + +### Usage + +Render a user interface to input the name of an additional css class. + + import { TextControl } from '@wordpress/components'; + import { withState } from '@wordpress/compose'; + + const MyTextControl = withState( { + className: '', + } )( ( { className, setState } ) => ( + setState( { className } ) } + /> + ) ); + +### Props The set of props accepted by the component will be specified below. Props not included in this set will be applied to the input element. -### label - +#### label If this property is added, a label will be generated using label property as the content. - Type: `String` - Required: No -### help - +#### help If this property is added, a help text will be generated using help property as the content. -- Type: `String|WPElement` +- Type: `String` - Required: No -### type - +#### type Type of the input element to render. Defaults to "text". - Type: `String` - Required: No - Default: "text" -### value - +#### value The current value of the input. - Type: `Number` - Required: Yes -### className - +#### className The class that will be added with "components-base-control" to the classes of the wrapper div. If no className is passed only components-base-control is used. - Type: `String` - Required: No -### onChange - +#### onChange A function that receives the value of the input. - Type: `function` - Required: Yes + +## Related components +- To offer users more constrained options for input, use SelectControl, RadioControl, CheckboxControl, or RangeControl. From 8d8a2854d797460fb04f472f648d5f25a0f8f264 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Sat, 30 Mar 2019 09:52:33 -0400 Subject: [PATCH 26/38] API Fetch: Fix error on empty OPTIONS preload data (#14714) --- .../api-fetch/src/middlewares/preloading.js | 6 +++- .../src/middlewares/test/preloading.js | 35 ++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/packages/api-fetch/src/middlewares/preloading.js b/packages/api-fetch/src/middlewares/preloading.js index 020cb913fced15..4de356243b7366 100644 --- a/packages/api-fetch/src/middlewares/preloading.js +++ b/packages/api-fetch/src/middlewares/preloading.js @@ -34,7 +34,11 @@ const createPreloadingMiddleware = ( preloadedData ) => ( options, next ) => { if ( parse && 'GET' === method && preloadedData[ path ] ) { return Promise.resolve( preloadedData[ path ].body ); - } else if ( 'OPTIONS' === method && preloadedData[ method ][ path ] ) { + } else if ( + 'OPTIONS' === method && + preloadedData[ method ] && + preloadedData[ method ][ path ] + ) { return Promise.resolve( preloadedData[ method ][ path ] ); } } diff --git a/packages/api-fetch/src/middlewares/test/preloading.js b/packages/api-fetch/src/middlewares/test/preloading.js index d697af3b429494..e2d8a1f4d0c68d 100644 --- a/packages/api-fetch/src/middlewares/test/preloading.js +++ b/packages/api-fetch/src/middlewares/test/preloading.js @@ -25,20 +25,29 @@ describe( 'Preloading Middleware', () => { } ); } ); - it( 'should move to the next middleware if no preloaded data', () => { - const preloadedData = {}; - const prelooadingMiddleware = createPreloadingMiddleware( preloadedData ); - const requestOptions = { - method: 'GET', - path: 'wp/v2/posts', - }; + describe.each( [ + [ 'GET' ], + [ 'OPTIONS' ], + ] )( '%s', ( method ) => { + describe.each( [ + [ 'all empty', {} ], + [ 'method empty', { [ method ]: {} } ], + ] )( '%s', ( label, preloadedData ) => { + it( 'should move to the next middleware if no preloaded data', () => { + const prelooadingMiddleware = createPreloadingMiddleware( preloadedData ); + const requestOptions = { + method, + path: 'wp/v2/posts', + }; - const callback = ( options ) => { - expect( options ).toBe( requestOptions ); - return true; - }; + const callback = ( options ) => { + expect( options ).toBe( requestOptions ); + return true; + }; - const ret = prelooadingMiddleware( requestOptions, callback ); - expect( ret ).toBe( true ); + const ret = prelooadingMiddleware( requestOptions, callback ); + expect( ret ).toBe( true ); + } ); + } ); } ); } ); From 18b43052961f7750f4ab16f9196de7e57429e936 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Sat, 30 Mar 2019 13:55:42 +0000 Subject: [PATCH 27/38] Removes unwanted theme specific Column bottom margin (#14614) --- packages/block-library/src/columns/style.scss | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/block-library/src/columns/style.scss b/packages/block-library/src/columns/style.scss index e377480792af02..2cc0cf0ec4999e 100644 --- a/packages/block-library/src/columns/style.scss +++ b/packages/block-library/src/columns/style.scss @@ -10,6 +10,7 @@ } .wp-block-column { + margin-bottom: 1em; flex-grow: 1; // Responsiveness: Show at most one columns on mobile. @@ -44,14 +45,6 @@ } } -// Specificity overide to ensure margin is applied -// and preserved on last child to ensure that when columns -// are aligned to bottom they are are flush with each other -.wp-block-column, -.entry-content > .wp-block-columns .wp-block-column:last-child { - margin-bottom: 1em; -} - /** * All Columns Alignment */ From 14d0e2c2a380740f026b915a691e9e9c541e9d84 Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Mon, 1 Apr 2019 12:37:04 +1100 Subject: [PATCH 28/38] Add LGPL as an OSS license. (#14734) --- packages/scripts/scripts/check-licenses.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/scripts/scripts/check-licenses.js b/packages/scripts/scripts/check-licenses.js index 54b906f30e37b7..7670ae3b0d2140 100644 --- a/packages/scripts/scripts/check-licenses.js +++ b/packages/scripts/scripts/check-licenses.js @@ -79,6 +79,7 @@ const otherOssLicenses = [ 'Apache License, Version 2.0', 'Apache version 2.0', 'CC-BY-3.0', + 'LGPL', ]; const licenses = [ From 806da4a890dfde05693834dd1a9ba94f14118992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Wrede?= Date: Mon, 1 Apr 2019 04:14:18 +0200 Subject: [PATCH 29/38] Fix verse icon (#14723) --- packages/block-library/src/verse/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/verse/index.js b/packages/block-library/src/verse/index.js index 4016915ad6689c..ab5434192877e8 100644 --- a/packages/block-library/src/verse/index.js +++ b/packages/block-library/src/verse/index.js @@ -18,7 +18,7 @@ export const settings = { description: __( 'Insert poetry. Use special spacing formats. Or quote song lyrics.' ), - icon: , + icon: , category: 'formatting', From 603709defcd33d74da1327abbe6715c2f4c7ea28 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Mon, 1 Apr 2019 14:24:37 +1100 Subject: [PATCH 30/38] Bump plugin version to 5.4.0-rc.1 (#14735) --- gutenberg.php | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 549d2251b17a3f..90b1961840fd19 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * 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. - * Version: 5.3.0 + * Version: 5.4.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 6029431dd88175..40ebb372807de8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "5.3.0", + "version": "5.4.0-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f63075eaa1525d..0f979954ba2dc0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "5.3.0", + "version": "5.4.0-rc.1", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", From 0d58c7092074ea93b05662fb5c2050c613aca24f Mon Sep 17 00:00:00 2001 From: Michael Chavez Date: Mon, 1 Apr 2019 00:11:33 -0700 Subject: [PATCH 31/38] Change 'dependencies' to 'devDependencies'. (#14736) Because it says to run npm install with --save-dev flag "dependencies": { "@wordpress/scripts": "3.1.0" } ``` Should say: ``` "devDependencies": { "@wordpress/scripts": "3.1.0" } ``` --- .../developers/tutorials/javascript/js-build-setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designers-developers/developers/tutorials/javascript/js-build-setup.md b/docs/designers-developers/developers/tutorials/javascript/js-build-setup.md index 64f513d605cf99..134c3046ab3149 100644 --- a/docs/designers-developers/developers/tutorials/javascript/js-build-setup.md +++ b/docs/designers-developers/developers/tutorials/javascript/js-build-setup.md @@ -86,7 +86,7 @@ After installing, a `node_modules` directory is created with the modules and the Also, if you look at package.json file it will include a new section: ```json -"dependencies": { +"devDependencies": { "@wordpress/scripts": "3.1.0" } ``` From f5f77d5bdf13e9efef69d7427194a04ab277df32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= Date: Mon, 1 Apr 2019 09:52:46 +0200 Subject: [PATCH 32/38] Ensure that insertUsage is always present in preferences (#14706) --- packages/editor/src/store/defaults.js | 1 + packages/editor/src/store/test/reducer.js | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js index d22d6e7f67de8c..fb49621e8e5a64 100644 --- a/packages/editor/src/store/defaults.js +++ b/packages/editor/src/store/defaults.js @@ -4,6 +4,7 @@ import { SETTINGS_DEFAULTS } from '@wordpress/block-editor'; export const PREFERENCES_DEFAULTS = { + insertUsage: {}, // Should be kept for backward compatibility, see: https://github.com/WordPress/gutenberg/issues/14580. isPublishSidebarEnabled: true, }; diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index f7be763e12170f..751563e77ecc3c 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -481,6 +481,7 @@ describe( 'state', () => { it( 'should apply all defaults', () => { const state = preferences( undefined, {} ); expect( state ).toEqual( { + insertUsage: {}, isPublishSidebarEnabled: true, } ); } ); From d32993c61d9e72785b6b81871438c84c77b7058c Mon Sep 17 00:00:00 2001 From: Stephen Edgar Date: Mon, 1 Apr 2019 19:52:33 +1100 Subject: [PATCH 33/38] nvm is not an npm module (#14737) --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d2f9e9ec7fe6f7..f3c371c089f38a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,8 +21,8 @@ For another version of Windows, or if you prefer to set things up manually, be s If you have an incompatible version of Node in your development environment, you can use [nvm](https://github.com/creationix/nvm) to change node versions on the command line: ``` -npx nvm install -npx nvm use +nvm install +nvm use ``` You also should have the latest release of [npm installed][npm]. npm is a separate project from Node.js and is updated frequently. If you've just installed Node.js which includes a version of npm within the installation you most likely will need also to update your npm installation. To update npm, type this into your terminal: `npm install npm@latest -g` From fcbeeccb2726c4e0a4075692133783c1aedba7eb Mon Sep 17 00:00:00 2001 From: Marty Helmick Date: Mon, 1 Apr 2019 04:58:59 -0400 Subject: [PATCH 34/38] remove unnecessary bottom margin from figcaption (#14731) --- packages/block-library/src/style.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index 5599dafd7c463f..285c139b3b0fa4 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -150,5 +150,4 @@ // By providing a minimum of margin styles, we ensure it doesn't look broken or unstyled in those themes. figcaption { margin-top: 0.5em; - margin-bottom: 1em; } From a61e351234d9c74f9fe80b35f6f97769e0758d78 Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Mon, 1 Apr 2019 13:10:58 +0300 Subject: [PATCH 35/38] [14494] - Improve e2e testing docs and add cli args. (#14717) * Updated readme and added local arg. * Lint fixes. * Replaced dumb namings with a more reasonable ones. * Added --wordpress- to cleanUpPrefixes. * Align argument name with env key. --- packages/scripts/README.md | 3 +++ packages/scripts/scripts/test-e2e.js | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 0c1687ed49f9c2..e6bc450c54ba00 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -216,6 +216,9 @@ This is how you execute those scripts using the presented setup: * `npm run test:e2e` - runs all unit tests. * `npm run test:e2e:help` - prints all available options to configure unit tests runner. +* `npm run test-e2e -- --puppeteer-interactive` - runs all unit tests interactively. +* `npm run test-e2e FILE_NAME -- --puppeteer-interactive ` - runs one test file interactively. +* `npm run test-e2e:watch -- --puppeteer-interactive` - runs all tests interactively and watch for changes. This script automatically detects the best config to start Puppeteer but sometimes you may need to specify custom options: - You can add a `jest-puppeteer.config.js` at the root of the project or define a custom path using `JEST_PUPPETEER_CONFIG` environment variable. Check [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer#jest-puppeteerconfigjs) for more details. diff --git a/packages/scripts/scripts/test-e2e.js b/packages/scripts/scripts/test-e2e.js index 8105891c8d838b..374ebc59f6fbe5 100644 --- a/packages/scripts/scripts/test-e2e.js +++ b/packages/scripts/scripts/test-e2e.js @@ -43,11 +43,23 @@ const runInBand = ! hasRunInBand ? [ '--runInBand' ] : []; -const cleanUpPrefixes = [ '--puppeteer-' ]; - if ( hasCliArg( '--puppeteer-interactive' ) ) { process.env.PUPPETEER_HEADLESS = 'false'; process.env.PUPPETEER_SLOWMO = getCliArg( '--puppeteer-slowmo' ) || 80; } +const configsMapping = { + WP_BASE_URL: '--wordpress-base-url', + WP_USERNAME: '--wordpress-username', + WP_PASSWORD: '--wordpress-password', +}; + +Object.entries( configsMapping ).forEach( ( [ envKey, argName ] ) => { + if ( hasCliArg( argName ) ) { + process.env[ envKey ] = getCliArg( argName ); + } +} ); + +const cleanUpPrefixes = [ '--puppeteer-', '--wordpress-' ]; + jest.run( [ ...config, ...runInBand, ...getCliArgs( cleanUpPrefixes ) ] ); From 1ef4cfd50ffe54c884937f24ec7ff1ba1de50b06 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 21 Mar 2019 16:43:29 +0100 Subject: [PATCH 36/38] Block library: Try to use Babel plugins to inline block.json metadata --- babel.config.js | 1 + package-lock.json | 24 ++++++++++++ package.json | 1 + packages/block-library/src/index.js | 6 ++- .../block-library/src/text-columns/block.json | 29 ++++++++++++++ .../block-library/src/text-columns/index.js | 38 +++---------------- packages/blocks/src/api/index.js | 1 + packages/blocks/src/api/registration.js | 9 +++++ playground/.babelrc | 5 ++- 9 files changed, 79 insertions(+), 35 deletions(-) create mode 100644 packages/block-library/src/text-columns/block.json diff --git a/babel.config.js b/babel.config.js index b56ad5b149478b..7679cc1e837041 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,5 +3,6 @@ module.exports = function( api ) { return { presets: [ '@wordpress/babel-preset-default' ], + plugins: [ 'babel-plugin-inline-json-import' ], }; }; diff --git a/package-lock.json b/package-lock.json index 40ebb372807de8..3d13faa76d4059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4065,6 +4065,15 @@ } } }, + "babel-plugin-inline-json-import": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/babel-plugin-inline-json-import/-/babel-plugin-inline-json-import-0.3.2.tgz", + "integrity": "sha512-QNNJx08KjmMT25Cw7rAPQ6dlREDPiZGDyApHL8KQ9vrQHbrr4PTi7W8g1tMMZPz0jEMd39nx/eH7xjnDNxq5sA==", + "dev": true, + "requires": { + "decache": "^4.5.1" + } + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -4699,6 +4708,12 @@ "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", "dev": true }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, "callsites": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz", @@ -7211,6 +7226,15 @@ "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=", "dev": true }, + "decache": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/decache/-/decache-4.5.1.tgz", + "integrity": "sha512-5J37nATc6FmOTLbcsr9qx7Nm28qQyg1SK4xyEHqM0IBkNhWFp0Sm+vKoWYHD8wq+OUEb9jLyaKFfzzd1A9hcoA==", + "dev": true, + "requires": { + "callsite": "^1.0.0" + } + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", diff --git a/package.json b/package.json index 0f979954ba2dc0..ee92bafcfda43d 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@wordpress/npm-package-json-lint-config": "file:packages/npm-package-json-lint-config", "@wordpress/postcss-themes": "file:packages/postcss-themes", "@wordpress/scripts": "file:packages/scripts", + "babel-plugin-inline-json-import": "0.3.2", "benchmark": "2.1.4", "browserslist": "4.4.1", "chalk": "2.4.1", diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 81a0e9bec632b4..9255287273d633 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -9,6 +9,7 @@ import { setDefaultBlockName, setFreeformContentHandlerName, setUnregisteredTypeHandlerName, + unstable__setBlockMetadata, // eslint-disable-line camelcase } from '@wordpress/blocks'; /** @@ -120,7 +121,10 @@ export const registerCoreBlocks = () => { if ( ! block ) { return; } - const { name, settings } = block; + const { metadata, settings, name = metadata.name } = block; + if ( metadata ) { + unstable__setBlockMetadata( metadata ); // eslint-disable-line camelcase + } registerBlockType( name, settings ); } ); diff --git a/packages/block-library/src/text-columns/block.json b/packages/block-library/src/text-columns/block.json new file mode 100644 index 00000000000000..6a70d04700780c --- /dev/null +++ b/packages/block-library/src/text-columns/block.json @@ -0,0 +1,29 @@ +{ + "name": "core/text-columns", + "icon": "columns", + "category": "layout", + "attributes": { + "content": { + "type": "array", + "source": "query", + "selector": "p", + "query": { + "children": { + "type": "string", + "source": "html" + } + }, + "default": [ { }, { } ] + }, + "columns": { + "type": "number", + "default": 2 + }, + "width": { + "type": "string" + } + }, + "supports": { + "inserter": false + } +} diff --git a/packages/block-library/src/text-columns/index.js b/packages/block-library/src/text-columns/index.js index f14b3b8153baa8..96c63a345246fe 100644 --- a/packages/block-library/src/text-columns/index.js +++ b/packages/block-library/src/text-columns/index.js @@ -18,44 +18,18 @@ import { } from '@wordpress/block-editor'; import deprecated from '@wordpress/deprecated'; -export const name = 'core/text-columns'; +/** + * Internal dependencies + */ +import metadata from './block.json'; -export const settings = { - // Disable insertion as this block is deprecated and ultimately replaced by the Columns block. - supports: { - inserter: false, - }, +export { metadata }; +export const settings = { title: __( 'Text Columns (deprecated)' ), description: __( 'This block is deprecated. Please use the Columns block instead.' ), - icon: 'columns', - - category: 'layout', - - attributes: { - content: { - type: 'array', - source: 'query', - selector: 'p', - query: { - children: { - type: 'string', - source: 'html', - }, - }, - default: [ {}, {} ], - }, - columns: { - type: 'number', - default: 2, - }, - width: { - type: 'string', - }, - }, - transforms: { to: [ { diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index c13b28f4903ca0..9741ec6541b3f6 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -44,6 +44,7 @@ export { hasChildBlocks, hasChildBlocksWithInserterSupport, unstable__bootstrapServerSideBlockDefinitions, // eslint-disable-line camelcase + unstable__setBlockMetadata, // eslint-disable-line camelcase registerBlockStyle, unregisterBlockStyle, } from './registration'; diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index f6d5a29544803c..8e27a97bd9511e 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -57,6 +57,15 @@ export function unstable__bootstrapServerSideBlockDefinitions( definitions ) { / serverSideBlockDefinitions = definitions; } +/** + * Sets the block's metadata. + * + * @param {Object} definitions Server-side block definitions + */ +export function unstable__setBlockMetadata( { name, ...settings } ) { // eslint-disable-line camelcase + serverSideBlockDefinitions[ name ] = settings; +} + /** * Registers a new block provided a unique name and an object defining its * behavior. Once registered, the block is made available as an option to any diff --git a/playground/.babelrc b/playground/.babelrc index 4e7ed80cab13cd..5b8ba9724e13af 100644 --- a/playground/.babelrc +++ b/playground/.babelrc @@ -3,6 +3,7 @@ "plugins": [ [ "@babel/plugin-transform-react-jsx", { "pragma": "createElement" - } ] + } ], + "babel-plugin-inline-json-import" ] -} \ No newline at end of file +} From 80ac2755b78514bc85c0c39c8b9e09751ea5f703 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Wed, 27 Mar 2019 15:31:50 +0100 Subject: [PATCH 37/38] Address feedback from review --- packages/block-library/src/index.js | 4 ++-- .../block-library/src/text-columns/block.json | 3 --- packages/block-library/src/text-columns/index.js | 5 +++++ packages/blocks/src/api/index.js | 1 - packages/blocks/src/api/registration.js | 16 +++++----------- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 9255287273d633..263f37011cf812 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -9,7 +9,7 @@ import { setDefaultBlockName, setFreeformContentHandlerName, setUnregisteredTypeHandlerName, - unstable__setBlockMetadata, // eslint-disable-line camelcase + unstable__bootstrapServerSideBlockDefinitions, // eslint-disable-line camelcase } from '@wordpress/blocks'; /** @@ -123,7 +123,7 @@ export const registerCoreBlocks = () => { } const { metadata, settings, name = metadata.name } = block; if ( metadata ) { - unstable__setBlockMetadata( metadata ); // eslint-disable-line camelcase + unstable__bootstrapServerSideBlockDefinitions( { [ name ]: metadata } ); // eslint-disable-line camelcase } registerBlockType( name, settings ); } ); diff --git a/packages/block-library/src/text-columns/block.json b/packages/block-library/src/text-columns/block.json index 6a70d04700780c..c746279cc5df90 100644 --- a/packages/block-library/src/text-columns/block.json +++ b/packages/block-library/src/text-columns/block.json @@ -22,8 +22,5 @@ "width": { "type": "string" } - }, - "supports": { - "inserter": false } } diff --git a/packages/block-library/src/text-columns/index.js b/packages/block-library/src/text-columns/index.js index 96c63a345246fe..a509d95e274686 100644 --- a/packages/block-library/src/text-columns/index.js +++ b/packages/block-library/src/text-columns/index.js @@ -26,6 +26,11 @@ import metadata from './block.json'; export { metadata }; export const settings = { + // Disable insertion as this block is deprecated and ultimately replaced by the Columns block. + supports: { + inserter: false, + }, + title: __( 'Text Columns (deprecated)' ), description: __( 'This block is deprecated. Please use the Columns block instead.' ), diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 9741ec6541b3f6..c13b28f4903ca0 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -44,7 +44,6 @@ export { hasChildBlocks, hasChildBlocksWithInserterSupport, unstable__bootstrapServerSideBlockDefinitions, // eslint-disable-line camelcase - unstable__setBlockMetadata, // eslint-disable-line camelcase registerBlockStyle, unregisterBlockStyle, } from './registration'; diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 8e27a97bd9511e..61b1d8f2c3c442 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -49,21 +49,15 @@ import { isValidIcon, normalizeIconObject } from './utils'; let serverSideBlockDefinitions = {}; /** - * Set the server side block definition of blocks. + * Sets the server side block definition of blocks. * * @param {Object} definitions Server-side block definitions */ export function unstable__bootstrapServerSideBlockDefinitions( definitions ) { // eslint-disable-line camelcase - serverSideBlockDefinitions = definitions; -} - -/** - * Sets the block's metadata. - * - * @param {Object} definitions Server-side block definitions - */ -export function unstable__setBlockMetadata( { name, ...settings } ) { // eslint-disable-line camelcase - serverSideBlockDefinitions[ name ] = settings; + serverSideBlockDefinitions = { + ...serverSideBlockDefinitions, + ...definitions, + }; } /** From 98786ad3c902384251b21f74f63b2784aaff3647 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Mon, 1 Apr 2019 13:34:39 +0200 Subject: [PATCH 38/38] Address feedback from code review --- packages/block-library/src/index.js | 2 +- packages/block-library/src/text-columns/block.json | 2 +- packages/block-library/src/text-columns/index.js | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 263f37011cf812..65c4dd6d4144f6 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -121,7 +121,7 @@ export const registerCoreBlocks = () => { if ( ! block ) { return; } - const { metadata, settings, name = metadata.name } = block; + const { metadata, settings, name } = block; if ( metadata ) { unstable__bootstrapServerSideBlockDefinitions( { [ name ]: metadata } ); // eslint-disable-line camelcase } diff --git a/packages/block-library/src/text-columns/block.json b/packages/block-library/src/text-columns/block.json index c746279cc5df90..cf86b62ec572c0 100644 --- a/packages/block-library/src/text-columns/block.json +++ b/packages/block-library/src/text-columns/block.json @@ -13,7 +13,7 @@ "source": "html" } }, - "default": [ { }, { } ] + "default": [ {}, {} ] }, "columns": { "type": "number", diff --git a/packages/block-library/src/text-columns/index.js b/packages/block-library/src/text-columns/index.js index a509d95e274686..6ec2e8b977109a 100644 --- a/packages/block-library/src/text-columns/index.js +++ b/packages/block-library/src/text-columns/index.js @@ -23,7 +23,9 @@ import deprecated from '@wordpress/deprecated'; */ import metadata from './block.json'; -export { metadata }; +const { name } = metadata; + +export { metadata, name }; export const settings = { // Disable insertion as this block is deprecated and ultimately replaced by the Columns block.