woocommerce/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/checkout-i2/hacks.ts

152 lines
3.9 KiB
TypeScript

/**
* HACKS
*
* This file contains functionality to "lock" blocks i.e. to prevent blocks being moved or deleted. This needs to be
* kept in place until native support for locking is available in WordPress (estimated WordPress 5.9).
*/
/**
* @todo Remove custom block locking (requires native WordPress support)
*/
/**
* External dependencies
*/
import {
useBlockProps,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { isTextField } from '@wordpress/dom';
import { useSelect, subscribe, select as _select } from '@wordpress/data';
import { useEffect, useRef } from '@wordpress/element';
import { MutableRefObject } from 'react';
import { BACKSPACE, DELETE } from '@wordpress/keycodes';
import { hasFilter } from '@wordpress/hooks';
/**
* Toggle class on body.
*
* @param {string} className CSS Class name.
* @param {boolean} add True to add, false to remove.
*/
const toggleBodyClass = ( className: string, add = true ) => {
if ( add ) {
window.document.body.classList.add( className );
} else {
window.document.body.classList.remove( className );
}
};
/**
* addClassToBody
*
* This components watches the current selected block and adds a class name to the body if that block is locked. If the
* current block is not locked, it removes the class name. The appended body class is used to hide UI elements to prevent
* the block from being deleted.
*
* We use a component so we can react to changes in the store.
*/
export const addClassToBody = (): void => {
if ( ! hasFilter( 'blocks.registerBlockType', 'core/lock/addAttribute' ) ) {
subscribe( () => {
const blockEditorSelect = _select( blockEditorStore );
if ( ! blockEditorSelect ) {
return;
}
const selectedBlock = blockEditorSelect.getSelectedBlock();
if ( ! selectedBlock ) {
return;
}
toggleBodyClass(
'wc-lock-selected-block--remove',
!! selectedBlock?.attributes?.lock?.remove
);
toggleBodyClass(
'wc-lock-selected-block--move',
!! selectedBlock?.attributes?.lock?.move
);
} );
}
};
/**
* This is a hook we use in conjunction with useBlockProps. Its goal is to check if a block is locked (move or remove)
* and will stop the keydown event from propagating to stop it from being deleted via the keyboard.
*
* @todo Disable custom locking support if native support is detected.
*/
const useLockBlock = ( {
clientId,
ref,
attributes,
}: {
clientId: string;
ref: MutableRefObject< Element | undefined >;
attributes: Record< string, unknown >;
} ): void => {
const lockInCore = hasFilter(
'blocks.registerBlockType',
'core/lock/addAttribute'
);
const { isSelected } = useSelect(
( select ) => {
return {
isSelected: select( blockEditorStore ).isBlockSelected(
clientId
),
};
},
[ clientId ]
);
const node = ref.current;
return useEffect( () => {
if ( ! isSelected || ! node || lockInCore ) {
return;
}
function onKeyDown( event: KeyboardEvent ) {
const { keyCode, target } = event;
if ( keyCode !== BACKSPACE && keyCode !== DELETE ) {
return;
}
if ( target !== node || isTextField( target ) ) {
return;
}
// Prevent the keyboard event from propogating if it supports locking.
if ( attributes?.lock?.remove ) {
event.preventDefault();
event.stopPropagation();
}
}
node.addEventListener( 'keydown', onKeyDown, true );
return () => {
node.removeEventListener( 'keydown', onKeyDown, true );
};
}, [ node, isSelected, lockInCore, attributes ] );
};
/**
* This hook is a light wrapper to useBlockProps, it wraps that hook plus useLockBlock to pass data between them.
*/
export const useBlockPropsWithLocking = (
props: Record< string, unknown > = {}
): Record< string, unknown > => {
const ref = useRef< Element >();
const { attributes } = props;
const blockProps = useBlockProps( { ref, ...props } );
useLockBlock( {
ref,
attributes,
clientId: blockProps[ 'data-block' ],
} );
return blockProps;
};