Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable multi select block attribute controls #22470

Merged
merged 3 commits into from
Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1395,12 +1395,12 @@ _Returns_

<a name="updateBlockAttributes" href="#updateBlockAttributes">#</a> **updateBlockAttributes**

Returns an action object used in signalling that the block attributes with
the specified client ID has been updated.
Returns an action object used in signalling that the multiple blocks'
attributes with the specified client IDs have been updated.

_Parameters_

- _clientId_ `string`: Block client ID.
- _clientIds_ `(string|Array<string>)`: Block client IDs.
- _attributes_ `Object`: Block attributes to be merged.

_Returns_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,6 @@ If you have settings that affects only selected content inside a block (example:
The Block Tab is shown in place of the Document Tab when a block is selected.

Similar to rendering a toolbar, if you include an `InspectorControls` element in the return value of your block type's `edit` function, those controls will be shown in the Settings Sidebar region.

Block controls rendered in both the toolbar and sidebar will also be used when
multiple blocks of the same type are selected.
5 changes: 2 additions & 3 deletions packages/block-editor/src/components/block-controls/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
/**
* Internal dependencies
*/
import { useBlockEditContext } from '../block-edit/context';
import useDisplayBlockControls from '../use-display-block-controls';

const { Fill, Slot } = createSlotFill( 'BlockControls' );

Expand All @@ -26,8 +26,7 @@ function BlockControlsSlot( props ) {
}

function BlockControlsFill( { controls, children } ) {
const { isSelected } = useBlockEditContext();
if ( ! isSelected ) {
if ( ! useDisplayBlockControls() ) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ const BlockInspector = ( {
showNoBlockSelectedMessage = true,
} ) => {
if ( count > 1 ) {
return <MultiSelectionInspector />;
return (
<div className="block-editor-block-inspector">
<MultiSelectionInspector />
<InspectorControls.Slot bubblesVirtually />
</div>
);
}

const isSelectedBlockUnregistered =
Expand Down
19 changes: 16 additions & 3 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ const applyWithSelect = withSelect(
getTemplateLock,
__unstableGetBlockWithoutInnerBlocks,
isNavigationMode,
getMultiSelectedBlockClientIds,
} = select( 'core/block-editor' );
const block = __unstableGetBlockWithoutInnerBlocks( clientId );
const isSelected = isBlockSelected( clientId );
Expand All @@ -247,6 +248,7 @@ const applyWithSelect = withSelect(
// the state. It happens now because the order in withSelect rendering
// is not correct.
const { name, attributes, isValid } = block || {};
const isFirstMultiSelected = isFirstMultiSelectedBlock( clientId );

// Do not add new properties here, use `useSelect` instead to avoid
// leaking new props to the public API (editor.BlockListBlock filter).
Expand All @@ -255,9 +257,12 @@ const applyWithSelect = withSelect(
isPartOfMultiSelection:
isBlockMultiSelected( clientId ) ||
isAncestorMultiSelected( clientId ),
isFirstMultiSelected: isFirstMultiSelectedBlock( clientId ),
isFirstMultiSelected,
isLastMultiSelected:
getLastMultiSelectedBlockClientId() === clientId,
multiSelectedClientIds: isFirstMultiSelected
? getMultiSelectedBlockClientIds()
: undefined,

// We only care about this prop when the block is selected
// Thus to avoid unnecessary rerenders we avoid updating the prop if
Expand Down Expand Up @@ -301,8 +306,16 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => {
// leaking new props to the public API (editor.BlockListBlock filter).
return {
setAttributes( newAttributes ) {
const { clientId } = ownProps;
updateBlockAttributes( clientId, newAttributes );
const {
clientId,
isFirstMultiSelected,
multiSelectedClientIds,
} = ownProps;
const clientIds = isFirstMultiSelected
? multiSelectedClientIds
: [ clientId ];

updateBlockAttributes( clientIds, newAttributes );
},
onInsertBlocks( blocks, index ) {
const { rootClientId } = ownProps;
Expand Down
21 changes: 9 additions & 12 deletions packages/block-editor/src/components/block-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function BlockToolbar( { hideDragHandle } ) {
blockType,
hasFixedToolbar,
isValid,
mode,
isVisual,
moverDirection,
} = useSelect( ( select ) => {
const { getBlockType } = select( 'core/blocks' );
Expand Down Expand Up @@ -56,14 +56,12 @@ export default function BlockToolbar( { hideDragHandle } ) {
getBlockType( getBlockName( selectedBlockClientId ) ),
hasFixedToolbar: getSettings().hasFixedToolbar,
rootClientId: blockRootClientId,
isValid:
selectedBlockClientIds.length === 1
? isBlockValid( selectedBlockClientIds[ 0 ] )
: null,
mode:
selectedBlockClientIds.length === 1
? getBlockMode( selectedBlockClientIds[ 0 ] )
: null,
isValid: selectedBlockClientIds.every( ( id ) =>
isBlockValid( id )
),
isVisual: selectedBlockClientIds.every(
( id ) => getBlockMode( id ) === 'visual'
),
moverDirection: __experimentalMoverDirection,
};
}, [] );
Expand Down Expand Up @@ -93,7 +91,7 @@ export default function BlockToolbar( { hideDragHandle } ) {
return null;
}

const shouldShowVisualToolbar = isValid && mode === 'visual';
const shouldShowVisualToolbar = isValid && isVisual;
const isMultiToolbar = blockClientIds.length > 1;

const animatedMoverStyles = {
Expand Down Expand Up @@ -144,8 +142,7 @@ export default function BlockToolbar( { hideDragHandle } ) {
</div>
) }
</div>

{ shouldShowVisualToolbar && ! isMultiToolbar && (
{ shouldShowVisualToolbar && (
<>
<BlockControls.Slot
bubblesVirtually
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ import { createSlotFill } from '@wordpress/components';
/**
* Internal dependencies
*/
import { useBlockEditContext } from '../block-edit/context';
import useDisplayBlockControls from '../use-display-block-controls';

const { Fill, Slot } = createSlotFill( 'InspectorControls' );

function InspectorControls( { children } ) {
const { isSelected } = useBlockEditContext();
return isSelected ? <Fill>{ children }</Fill> : null;
return useDisplayBlockControls() ? <Fill>{ children }</Fill> : null;
}

InspectorControls.Slot = Slot;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import { useBlockEditContext } from '../block-edit/context';

export default function useDisplayBlockControls() {
const { isSelected, clientId, name } = useBlockEditContext();
const isFirstAndSameTypeMultiSelected = useSelect(
( select ) => {
// Don't bother checking, see OR statement below.
if ( isSelected ) {
return;
}

const {
getBlockName,
isFirstMultiSelectedBlock,
getMultiSelectedBlockClientIds,
} = select( 'core/block-editor' );

if ( ! isFirstMultiSelectedBlock( clientId ) ) {
return false;
}

return getMultiSelectedBlockClientIds().every(
( id ) => getBlockName( id ) === name
);
},
[ clientId, name ]
);

return isSelected || isFirstAndSameTypeMultiSelected;
}
12 changes: 6 additions & 6 deletions packages/block-editor/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,18 @@ export function receiveBlocks( blocks ) {
}

/**
* Returns an action object used in signalling that the block attributes with
* the specified client ID has been updated.
* Returns an action object used in signalling that the multiple blocks'
* attributes with the specified client IDs have been updated.
*
* @param {string} clientId Block client ID.
* @param {Object} attributes Block attributes to be merged.
* @param {string|string[]} clientIds Block client IDs.
* @param {Object} attributes Block attributes to be merged.
*
* @return {Object} Action object.
*/
export function updateBlockAttributes( clientId, attributes ) {
export function updateBlockAttributes( clientIds, attributes ) {
return {
type: 'UPDATE_BLOCK_ATTRIBUTES',
clientId,
clientIds: castArray( clientIds ),
attributes,
};
}
Expand Down
76 changes: 47 additions & 29 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export function isUpdatingSameBlockAttribute( action, lastAction ) {
action.type === 'UPDATE_BLOCK_ATTRIBUTES' &&
lastAction !== undefined &&
lastAction.type === 'UPDATE_BLOCK_ATTRIBUTES' &&
action.clientId === lastAction.clientId &&
isEqual( action.clientIds, lastAction.clientIds ) &&
hasSameKeys( action.attributes, lastAction.attributes )
);
}
Expand Down Expand Up @@ -293,14 +293,21 @@ const withBlockCache = ( reducer ) => ( state = {}, action ) => {
break;
}
case 'UPDATE_BLOCK':
case 'UPDATE_BLOCK_ATTRIBUTES':
newState.cache = {
...newState.cache,
...fillKeysWithEmptyObject(
getBlocksWithParentsClientIds( [ action.clientId ] )
),
};
break;
case 'UPDATE_BLOCK_ATTRIBUTES':
newState.cache = {
...newState.cache,
...fillKeysWithEmptyObject(
getBlocksWithParentsClientIds( action.clientIds )
),
};
break;
case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN':
const parentClientIds = fillKeysWithEmptyObject(
getBlocksWithParentsClientIds( action.replacedClientIds )
Expand Down Expand Up @@ -810,40 +817,45 @@ export const blocks = flow(
},
};

case 'UPDATE_BLOCK_ATTRIBUTES':
// Ignore updates if block isn't known
if ( ! state[ action.clientId ] ) {
case 'UPDATE_BLOCK_ATTRIBUTES': {
// Avoid a state change if none of the block IDs are known.
if ( action.clientIds.every( ( id ) => ! state[ id ] ) ) {
return state;
}

// Consider as updates only changed values
const nextAttributes = reduce(
action.attributes,
( result, value, key ) => {
if ( value !== result[ key ] ) {
result = getMutateSafeObject(
state[ action.clientId ],
result
);
result[ key ] = value;
}

return result;
},
state[ action.clientId ]
const next = action.clientIds.reduce(
( accumulator, id ) => ( {
...accumulator,
[ id ]: reduce(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use the vanilla [].reduce here; we're already using it a few lines above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it's not an array that we reduce.

action.attributes,
( result, value, key ) => {
// Consider as updates only changed values.
if ( value !== result[ key ] ) {
result = getMutateSafeObject(
state[ id ],
result
);
result[ key ] = value;
}

return result;
},
state[ id ]
),
} ),
{}
);

// Skip update if nothing has been changed. The reference will
// match the original block if `reduce` had no changed values.
if ( nextAttributes === state[ action.clientId ] ) {
if (
action.clientIds.every(
( id ) => next[ id ] === state[ id ]
)
) {
return state;
}

// Otherwise replace attributes in state
return {
...state,
[ action.clientId ]: nextAttributes,
};
return { ...state, ...next };
}

case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN':
if ( ! action.blocks ) {
Expand Down Expand Up @@ -1541,7 +1553,13 @@ export function lastBlockAttributesChange( state, action ) {
return { [ action.clientId ]: action.updates.attributes };

case 'UPDATE_BLOCK_ATTRIBUTES':
return { [ action.clientId ]: action.attributes };
return action.clientIds.reduce(
( accumulator, id ) => ( {
...accumulator,
[ id ]: action.attributes,
} ),
{}
);
}

return null;
Expand Down
15 changes: 13 additions & 2 deletions packages/block-editor/src/store/test/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,24 @@ describe( 'actions', () => {
} );

describe( 'updateBlockAttributes', () => {
it( 'should return the UPDATE_BLOCK_ATTRIBUTES action', () => {
it( 'should return the UPDATE_BLOCK_ATTRIBUTES action (string)', () => {
const clientId = 'myclientid';
const attributes = {};
const result = updateBlockAttributes( clientId, attributes );
expect( result ).toEqual( {
type: 'UPDATE_BLOCK_ATTRIBUTES',
clientId,
clientIds: [ clientId ],
attributes,
} );
} );

it( 'should return the UPDATE_BLOCK_ATTRIBUTES action (array)', () => {
const clientIds = [ 'myclientid' ];
ellatrix marked this conversation as resolved.
Show resolved Hide resolved
const attributes = {};
const result = updateBlockAttributes( clientIds, attributes );
expect( result ).toEqual( {
type: 'UPDATE_BLOCK_ATTRIBUTES',
clientIds,
attributes,
} );
} );
Expand Down
Loading