@wordpress/block-editor
Version:
350 lines (313 loc) • 10.1 kB
JavaScript
/**
* WordPress dependencies
*/
import { isReusableBlock, isTemplatePart } from '@wordpress/blocks';
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,
getBlock,
} = 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();
// Remove the id and leave it on a shallow clone so that drop
// target calculations are correct.
const id = node.id;
const clone = node.cloneNode();
clone.style.display = 'none';
node.id = null;
node.after( clone );
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;
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;
let lastClientX = originClientX;
let lastClientY = originClientY;
function dragOver( e ) {
// Only trigger `over` if the mouse has moved.
if (
e.clientX === lastClientX &&
e.clientY === lastClientY
) {
return;
}
lastClientX = e.clientX;
lastClientY = e.clientY;
over();
}
function over() {
if ( ! hasStarted ) {
hasStarted = true;
node.style.pointerEvents = 'none';
}
const pointerYDelta = lastClientY - originClientY;
const pointerXDelta = lastClientX - originClientX;
const scrollTop = defaultView.scrollY;
const scrollLeft = defaultView.scrollX;
const scrollTopDelta = scrollTop - originScrollTop;
const scrollLeftDelta = scrollLeft - originScrollLeft;
const topDelta = pointerYDelta + scrollTopDelta;
const leftDelta = pointerXDelta + scrollLeftDelta;
node.style.top = `${ topDelta * inverted }px`;
node.style.left = `${ leftDelta * inverted }px`;
}
function end() {
ownerDocument.removeEventListener( 'dragover', dragOver );
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', dragOver );
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 block = getBlock( clientId );
const isSyncedPattern = isReusableBlock( block );
const isTemplatePartBlock = isTemplatePart( block );
const isAlreadyEditing = editedContentOnlySection === clientId;
if (
! isSection ||
isAlreadyEditing ||
isSyncedPattern ||
isTemplatePartBlock
) {
return;
}
event.preventDefault();
editContentOnlySection( clientId );
}
node.addEventListener( 'dblclick', onDoubleClick );
return () => {
node.removeEventListener( 'keydown', onKeyDown );
node.removeEventListener( 'dragstart', onDragStart );
node.removeEventListener( 'dblclick', onDoubleClick );
};
},
[
clientId,
isSelected,
getBlockRootClientId,
getBlock,
isReusableBlock,
isTemplatePart,
insertAfterBlock,
removeBlock,
isZoomOut,
resetZoomLevel,
hasMultiSelection,
startDraggingBlocks,
stopDraggingBlocks,
isSectionBlock,
editedContentOnlySection,
editContentOnlySection,
]
);
}