Skip to content

Commit

Permalink
Try: Reusable block edit locking (#39950)
Browse files Browse the repository at this point in the history
* Introduce canEditBlock selector

* Update BlockContentOverlay

* Update UI

* Fix indeterminate state and e2e tests

* Update list view

* Update selector

* Provide 'isLocked' value via hook
  • Loading branch information
Mamaduka authored Apr 11, 2022
1 parent fa15f5e commit f09a7b0
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 16 deletions.
13 changes: 13 additions & 0 deletions docs/reference-guides/data/data-core-block-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ _Returns_

- `boolean`: True if the block has controlled inner blocks.

### canEditBlock

Determines if the given block is allowed to be edited.

_Parameters_

- _state_ `Object`: Editor state.
- _clientId_ `string`: The block client Id.

_Returns_

- `boolean`: Whether the given block is allowed to be edited.

### canInsertBlocks

Determines if the given blocks are allowed to be inserted into the block
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default function BlockContentOverlay( {
const [ isHovered, setIsHovered ] = useState( false );

const {
canEdit,
isParentSelected,
hasChildSelected,
isDraggingBlocks,
Expand All @@ -36,8 +37,10 @@ export default function BlockContentOverlay( {
hasSelectedInnerBlock,
isDraggingBlocks: _isDraggingBlocks,
isBlockHighlighted,
canEditBlock,
} = select( blockEditorStore );
return {
canEdit: canEditBlock( clientId ),
isParentSelected: isBlockSelected( clientId ),
hasChildSelected: hasSelectedInnerBlock( clientId, true ),
isDraggingBlocks: _isDraggingBlocks(),
Expand All @@ -59,6 +62,12 @@ export default function BlockContentOverlay( {
);

useEffect( () => {
// The overlay is always active when editing is locked.
if ( ! canEdit ) {
setIsOverlayActive( true );
return;
}

// Reenable when blocks are not in use.
if ( ! isParentSelected && ! hasChildSelected && ! isOverlayActive ) {
setIsOverlayActive( true );
Expand All @@ -75,7 +84,13 @@ export default function BlockContentOverlay( {
if ( hasChildSelected && isOverlayActive ) {
setIsOverlayActive( false );
}
}, [ isParentSelected, hasChildSelected, isOverlayActive, isHovered ] );
}, [
isParentSelected,
hasChildSelected,
isOverlayActive,
isHovered,
canEdit,
] );

// Disabled because the overlay div doesn't actually have a role or functionality
// as far as the a11y is concerned. We're just catching the first click so that
Expand All @@ -88,7 +103,9 @@ export default function BlockContentOverlay( {
onMouseEnter={ () => setIsHovered( true ) }
onMouseLeave={ () => setIsHovered( false ) }
onMouseUp={
isOverlayActive ? () => setIsOverlayActive( false ) : undefined
isOverlayActive && canEdit
? () => setIsOverlayActive( false )
: undefined
}
>
{ wrapperProps?.children }
Expand Down
3 changes: 1 addition & 2 deletions packages/block-editor/src/components/block-lock/menu-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import useBlockLock from './use-block-lock';
import BlockLockModal from './modal';

export default function BlockLockMenuItem( { clientId } ) {
const { canMove, canRemove, canLock } = useBlockLock( clientId, true );
const isLocked = ! canMove || ! canRemove;
const { canLock, isLocked } = useBlockLock( clientId, true );

const [ isModalOpen, toggleModal ] = useReducer(
( isActive ) => ! isActive,
Expand Down
50 changes: 44 additions & 6 deletions packages/block-editor/src/components/block-lock/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
} from '@wordpress/components';
import { lock as lockIcon, unlock as unlockIcon } from '@wordpress/icons';
import { useInstanceId } from '@wordpress/compose';
import { useDispatch } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';
import { isReusableBlock, getBlockType } from '@wordpress/blocks';

/**
* Internal dependencies
Expand All @@ -24,7 +25,18 @@ import { store as blockEditorStore } from '../../store';

export default function BlockLockModal( { clientId, onClose } ) {
const [ lock, setLock ] = useState( { move: false, remove: false } );
const { canMove, canRemove } = useBlockLock( clientId, true );
const { canEdit, canMove, canRemove } = useBlockLock( clientId, true );
const { isReusable } = useSelect(
( select ) => {
const { getBlockName } = select( blockEditorStore );
const blockName = getBlockName( clientId );

return {
isReusable: isReusableBlock( getBlockType( blockName ) ),
};
},
[ clientId ]
);
const { updateBlockAttributes } = useDispatch( blockEditorStore );
const blockInformation = useBlockDisplayInformation( clientId );
const instanceId = useInstanceId(
Expand All @@ -36,12 +48,12 @@ export default function BlockLockModal( { clientId, onClose } ) {
setLock( {
move: ! canMove,
remove: ! canRemove,
...( isReusable ? { edit: ! canEdit } : {} ),
} );
}, [ canMove, canRemove ] );
}, [ canEdit, canMove, canRemove, isReusable ] );

const isAllChecked = Object.values( lock ).every( Boolean );
const isIndeterminate =
Object.values( lock ).some( Boolean ) && ! isAllChecked;
const isMixed = Object.values( lock ).some( Boolean ) && ! isAllChecked;

return (
<Modal
Expand Down Expand Up @@ -77,15 +89,41 @@ export default function BlockLockModal( { clientId, onClose } ) {
<span id={ instanceId }>{ __( 'Lock all' ) }</span>
}
checked={ isAllChecked }
indeterminate={ isIndeterminate }
indeterminate={ isMixed }
onChange={ ( newValue ) =>
setLock( {
move: newValue,
remove: newValue,
...( isReusable ? { edit: newValue } : {} ),
} )
}
/>
<ul className="block-editor-block-lock-modal__checklist">
{ isReusable && (
<li className="block-editor-block-lock-modal__checklist-item">
<CheckboxControl
label={
<>
{ __( 'Restrict editing' ) }
<Icon
icon={
lock.edit
? lockIcon
: unlockIcon
}
/>
</>
}
checked={ !! lock.edit }
onChange={ ( edit ) =>
setLock( ( prevLock ) => ( {
...prevLock,
edit,
} ) )
}
/>
</li>
) }
<li className="block-editor-block-lock-modal__checklist-item">
<CheckboxControl
label={
Expand Down
4 changes: 2 additions & 2 deletions packages/block-editor/src/components/block-lock/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import useBlockDisplayInformation from '../use-block-display-information';

export default function BlockLockToolbar( { clientId } ) {
const blockInformation = useBlockDisplayInformation( clientId );
const { canMove, canRemove, canLock } = useBlockLock( clientId );
const { canEdit, canMove, canRemove, canLock } = useBlockLock( clientId );

const [ isModalOpen, toggleModal ] = useReducer(
( isActive ) => ! isActive,
Expand All @@ -26,7 +26,7 @@ export default function BlockLockToolbar( { clientId } ) {
return null;
}

if ( canMove && canRemove ) {
if ( canEdit && canMove && canRemove ) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default function useBlockLock( clientId, checkParent = false ) {
return useSelect(
( select ) => {
const {
canEditBlock,
canMoveBlock,
canRemoveBlock,
canLockBlockType,
Expand All @@ -31,10 +32,16 @@ export default function useBlockLock( clientId, checkParent = false ) {
? getBlockRootClientId( clientId )
: null;

const canEdit = canEditBlock( clientId );
const canMove = canMoveBlock( clientId, rootClientId );
const canRemove = canRemoveBlock( clientId, rootClientId );

return {
canMove: canMoveBlock( clientId, rootClientId ),
canRemove: canRemoveBlock( clientId, rootClientId ),
canEdit,
canMove,
canRemove,
canLock: canLockBlockType( getBlockName( clientId ) ),
isLocked: ! canEdit || ! canMove || ! canRemove,
};
},
[ clientId, checkParent ]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ function ListViewBlockSelectButton(
ref
) {
const blockInformation = useBlockDisplayInformation( clientId );
const { canMove, canRemove } = useBlockLock( clientId );
const isLocked = ! canMove || ! canRemove;
const { isLocked } = useBlockLock( clientId );

// The `href` attribute triggers the browser's native HTML drag operations.
// When the link is dragged, the element's outerHTML is set in DataTransfer object as text/html.
Expand Down
20 changes: 20 additions & 0 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1569,6 +1569,26 @@ export function canMoveBlocks( state, clientIds, rootClientId = null ) {
);
}

/**
* Determines if the given block is allowed to be edited.
*
* @param {Object} state Editor state.
* @param {string} clientId The block client Id.
*
* @return {boolean} Whether the given block is allowed to be edited.
*/
export function canEditBlock( state, clientId ) {
const attributes = getBlockAttributes( state, clientId );
if ( attributes === null ) {
return true;
}

const { lock } = attributes;

// When the edit is true, we cannot edit the block.
return ! lock?.edit;
}

/**
* Determines if the given block type can be locked/unlocked by a user.
*
Expand Down

0 comments on commit f09a7b0

Please sign in to comment.