@wordpress/block-editor
Version:
335 lines (298 loc) • 9.51 kB
JavaScript
/**
* WordPress dependencies
*/
import { isTextField } from '@wordpress/dom';
import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes';
import { useSelect, useDispatch } from '@wordpress/data';
import { useRefEffect } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../../store';
import { unlock } from '../../../lock-unlock';
function isColorTransparent( color ) {
return ! color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';
}
/**
* Adds block behaviour:
* - Removes the block on BACKSPACE.
* - Inserts a default block on ENTER.
* - Disables dragging of block contents.
*
* @param {string} clientId Block client ID.
*/
export function useEventHandlers( { clientId, isSelected } ) {
const {
getBlockRootClientId,
isZoomOut,
hasMultiSelection,
isSectionBlock,
editedContentOnlySection,
} = unlock( useSelect( blockEditorStore ) );
const {
insertAfterBlock,
removeBlock,
resetZoomLevel,
startDraggingBlocks,
stopDraggingBlocks,
editContentOnlySection,
} = unlock( useDispatch( blockEditorStore ) );
return useRefEffect(
( node ) => {
if ( ! isSelected ) {
return;
}
/**
* Interprets keydown event intent to remove or insert after block if
* key event occurs on wrapper node. This can occur when the block has
* no text fields of its own, particularly after initial insertion, to
* allow for easy deletion and continuous writing flow to add additional
* content.
*
* @param {KeyboardEvent} event Keydown event.
*/
function onKeyDown( event ) {
const { keyCode, target } = event;
if (
keyCode !== ENTER &&
keyCode !== BACKSPACE &&
keyCode !== DELETE
) {
return;
}
if ( target !== node || isTextField( target ) ) {
return;
}
event.preventDefault();
if ( keyCode === ENTER && isZoomOut() ) {
resetZoomLevel();
} else if ( keyCode === ENTER ) {
insertAfterBlock( clientId );
} else {
removeBlock( clientId );
}
}
/**
* Prevents default dragging behavior within a block. To do: we must
* handle this in the future and clean up the drag target.
*
* @param {DragEvent} event Drag event.
*/
function onDragStart( event ) {
if (
node !== event.target ||
node.isContentEditable ||
node.ownerDocument.activeElement !== node ||
hasMultiSelection()
) {
event.preventDefault();
return;
}
const data = JSON.stringify( {
type: 'block',
srcClientIds: [ clientId ],
srcRootClientId: getBlockRootClientId( clientId ),
} );
event.dataTransfer.effectAllowed = 'move'; // remove "+" cursor
event.dataTransfer.clearData();
event.dataTransfer.setData( 'wp-blocks', data );
const { ownerDocument } = node;
const { defaultView } = ownerDocument;
const selection = defaultView.getSelection();
selection.removeAllRanges();
// Setting the drag chip as the drag image actually works, but
// the behaviour is slightly different in every browser. In
// Safari, it animates, in Firefox it's slightly transparent...
// So we set a fake drag image and have to reposition it
// ourselves.
const dragElement = ownerDocument.createElement( 'div' );
// Chrome will show a globe icon if the drag element does not
// have dimensions.
dragElement.style.width = '1px';
dragElement.style.height = '1px';
dragElement.style.position = 'fixed';
dragElement.style.visibility = 'hidden';
ownerDocument.body.appendChild( dragElement );
event.dataTransfer.setDragImage( dragElement, 0, 0 );
const rect = node.getBoundingClientRect();
const clone = node.cloneNode( true );
clone.style.visibility = 'hidden';
// Maybe remove the clone now that it's relative?
clone.style.display = 'none';
// Remove the id and leave it on the clone so that drop target
// calculations are correct.
const id = node.id;
node.id = null;
let _scale = 1;
{
let parentElement = node;
while ( ( parentElement = parentElement.parentElement ) ) {
const { scale } =
defaultView.getComputedStyle( parentElement );
if ( scale && scale !== 'none' ) {
_scale = parseFloat( scale );
break;
}
}
}
const inverted = 1 / _scale;
node.after( clone );
const originalNodeProperties = {};
for ( const property of [
'transform',
'transformOrigin',
'transition',
'zIndex',
'position',
'top',
'left',
'pointerEvents',
'opacity',
'backgroundColor',
] ) {
originalNodeProperties[ property ] = node.style[ property ];
}
// Get scroll position.
const originScrollTop = defaultView.scrollY;
const originScrollLeft = defaultView.scrollX;
const originClientX = event.clientX;
const originClientY = event.clientY;
// We can't use position fixed because it will behave different
// if the html element is scaled or transformed (position will
// no longer be relative to the viewport). The downside of
// relative is that we have to listen to scroll events. On the
// upside we don't have to clone to keep a space. Absolute
// positioning might be weird because it will be based on the
// positioned parent, but it might be worth a try.
node.style.position = 'relative';
node.style.top = `${ 0 }px`;
node.style.left = `${ 0 }px`;
const originX = event.clientX - rect.left;
const originY = event.clientY - rect.top;
// Scale everything to 200px.
const dragScale = rect.height > 200 ? 200 / rect.height : 1;
node.style.zIndex = '1000';
node.style.transformOrigin = `${ originX * inverted }px ${
originY * inverted
}px`;
node.style.transition = 'transform 0.2s ease-out';
node.style.transform = `scale(${ dragScale })`;
node.style.opacity = '0.9';
// If the block has no background color, use the parent's
// background color.
if (
isColorTransparent(
defaultView.getComputedStyle( node ).backgroundColor
)
) {
let bgColor = 'transparent';
let parentElement = node;
while ( ( parentElement = parentElement.parentElement ) ) {
const { backgroundColor } =
defaultView.getComputedStyle( parentElement );
if ( ! isColorTransparent( backgroundColor ) ) {
bgColor = backgroundColor;
break;
}
}
node.style.backgroundColor = bgColor;
}
let hasStarted = false;
function over( e ) {
if ( ! hasStarted ) {
hasStarted = true;
node.style.pointerEvents = 'none';
}
const scrollTop = defaultView.scrollY;
const scrollLeft = defaultView.scrollX;
node.style.top = `${
( e.clientY -
originClientY +
scrollTop -
originScrollTop ) *
inverted
}px`;
node.style.left = `${
( e.clientX -
originClientX +
scrollLeft -
originScrollLeft ) *
inverted
}px`;
}
function end() {
ownerDocument.removeEventListener( 'dragover', over );
ownerDocument.removeEventListener( 'dragend', end );
ownerDocument.removeEventListener( 'drop', end );
ownerDocument.removeEventListener( 'scroll', over );
for ( const [ property, value ] of Object.entries(
originalNodeProperties
) ) {
node.style[ property ] = value;
}
clone.remove();
node.id = id;
dragElement.remove();
stopDraggingBlocks();
document.body.classList.remove(
'is-dragging-components-draggable'
);
ownerDocument.documentElement.classList.remove(
'is-dragging'
);
}
ownerDocument.addEventListener( 'dragover', over );
ownerDocument.addEventListener( 'dragend', end );
ownerDocument.addEventListener( 'drop', end );
ownerDocument.addEventListener( 'scroll', over );
startDraggingBlocks( [ clientId ] );
// Important because it hides the block toolbar.
document.body.classList.add(
'is-dragging-components-draggable'
);
ownerDocument.documentElement.classList.add( 'is-dragging' );
}
node.addEventListener( 'keydown', onKeyDown );
node.addEventListener( 'dragstart', onDragStart );
/**
* Handles double-click events on section blocks to edit content only section.
*
* @param {MouseEvent} event Double-click event.
*/
function onDoubleClick( event ) {
const isSection = isSectionBlock( clientId );
const isAlreadyEditing = editedContentOnlySection === clientId;
if ( isSection && ! isAlreadyEditing ) {
event.preventDefault();
editContentOnlySection( clientId );
}
}
// Only add double-click listener if experimental flag is enabled
if ( window?.__experimentalContentOnlyPatternInsertion ) {
node.addEventListener( 'dblclick', onDoubleClick );
}
return () => {
node.removeEventListener( 'keydown', onKeyDown );
node.removeEventListener( 'dragstart', onDragStart );
if ( window?.__experimentalContentOnlyPatternInsertion ) {
node.removeEventListener( 'dblclick', onDoubleClick );
}
};
},
[
clientId,
isSelected,
getBlockRootClientId,
insertAfterBlock,
removeBlock,
isZoomOut,
resetZoomLevel,
hasMultiSelection,
startDraggingBlocks,
stopDraggingBlocks,
isSectionBlock,
editedContentOnlySection,
editContentOnlySection,
]
);
}