Skip to content

Commit

Permalink
Block editor: try direct drag (outside text editable) (#67305)
Browse files Browse the repository at this point in the history
Co-authored-by: ellatrix <[email protected]>
Co-authored-by: youknowriad <[email protected]>
Co-authored-by: jasmussen <[email protected]>
Co-authored-by: draganescu <[email protected]>
  • Loading branch information
5 people authored Nov 29, 2024
1 parent 11262ac commit 9ffbd9c
Show file tree
Hide file tree
Showing 16 changed files with 300 additions and 50 deletions.
16 changes: 11 additions & 5 deletions packages/block-editor/src/components/block-draggable/content.scss
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
// This creates a "slot" where the block you're dragging appeared.
// We use !important as one of the rules are meant to be overridden.
.block-editor-block-list__layout .is-dragging {
background-color: currentColor !important;
opacity: 0.05 !important;
opacity: 0.1 !important;
border-radius: $radius-small !important;

// Disabling pointer events during the drag event is necessary,
// lest the block might affect your drag operation.
pointer-events: none !important;
iframe {
pointer-events: none;
}

// Hide the multi selection indicator when dragging.
&::selection {
Expand All @@ -18,3 +17,10 @@
content: none !important;
}
}

// Images are draggable by default, so disable drag for them if not explicitly
// set. This is done so that the block can capture the drag event instead.
.wp-block img:not([draggable]),
.wp-block svg:not([draggable]) {
pointer-events: none;
}
1 change: 1 addition & 0 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,7 @@ function BlockListBlockProvider( props ) {
mayDisplayParentControls,
originalBlockClientId,
themeSupportsLayout,
canMove,
};

// Here we separate between the props passed to BlockListBlock and any other
Expand Down
6 changes: 6 additions & 0 deletions packages/block-editor/src/components/block-list/content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,9 @@ _::-webkit-full-page-media, _:future, :root [data-has-multi-selection="true"] .b
// Additional -1px is required to avoid sub pixel rounding errors allowing background to show.
margin: 0 calc(-1 * var(--wp--style--root--padding-right) - 1px) 0 calc(-1 * var(--wp--style--root--padding-left) - 1px) !important;
}

// This only works in Firefox, Chrome and Safari don't accept a custom cursor
// during drag.
.is-dragging {
cursor: grabbing;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { useIntersectionObserver } from './use-intersection-observer';
import { useScrollIntoView } from './use-scroll-into-view';
import { useFlashEditableBlocks } from '../../use-flash-editable-blocks';
import { canBindBlock } from '../../../hooks/use-bindings-attributes';
import { useFirefoxDraggableCompatibility } from './use-firefox-draggable-compatibility';

/**
* This hook is used to lightly mark an element as a block element. The element
Expand Down Expand Up @@ -100,11 +101,15 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) {
isTemporarilyEditingAsBlocks,
defaultClassName,
isSectionBlock,
canMove,
} = useContext( PrivateBlockContext );

const canDrag = canMove && ! hasChildSelected;

// translators: %s: Type of block (i.e. Text, Image etc)
const blockLabel = sprintf( __( 'Block: %s' ), blockTitle );
const htmlSuffix = mode === 'html' && ! __unstableIsHtml ? '-visual' : '';
const ffDragRef = useFirefoxDraggableCompatibility();
const mergedRefs = useMergeRefs( [
props.ref,
useFocusFirstElement( { clientId, initialPosition } ),
Expand All @@ -120,6 +125,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) {
isEnabled: isSectionBlock,
} ),
useScrollIntoView( { isSelected } ),
canDrag ? ffDragRef : undefined,
] );

const blockEditContext = useBlockEditContext();
Expand Down Expand Up @@ -152,6 +158,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) {

return {
tabIndex: blockEditingMode === 'disabled' ? -1 : 0,
draggable: canDrag ? true : undefined,
...wrapperProps,
...props,
ref: mergedRefs,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* WordPress dependencies
*/
import { useRefEffect } from '@wordpress/compose';

/**
* In Firefox, the `draggable` and `contenteditable` attributes don't play well
* together. When `contenteditable` is within a `draggable` element, selection
* doesn't get set in the right place. The only solution is to temporarily
* remove the `draggable` attribute clicking inside `contenteditable` elements.
*
* @return {Function} Cleanup function.
*/
export function useFirefoxDraggableCompatibility() {
return useRefEffect( ( node ) => {
function onDown( event ) {
node.draggable = ! event.target.isContentEditable;
}
const { ownerDocument } = node;
ownerDocument.addEventListener( 'pointerdown', onDown );
return () => {
ownerDocument.removeEventListener( 'pointerdown', onDown );
};
}, [] );
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { isTextField } from '@wordpress/dom';
import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes';
import { useSelect, useDispatch } from '@wordpress/data';
import { useRefEffect } from '@wordpress/compose';
import { createRoot } from '@wordpress/element';
import { store as blocksStore } from '@wordpress/blocks';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../../store';
import { unlock } from '../../../lock-unlock';
import BlockDraggableChip from '../../../components/block-draggable/draggable-chip';

/**
* Adds block behaviour:
Expand All @@ -21,12 +24,16 @@ import { unlock } from '../../../lock-unlock';
* @param {string} clientId Block client ID.
*/
export function useEventHandlers( { clientId, isSelected } ) {
const { getBlockRootClientId, getBlockIndex, isZoomOut } = unlock(
useSelect( blockEditorStore )
);
const { insertAfterBlock, removeBlock, resetZoomLevel } = unlock(
useDispatch( blockEditorStore )
);
const { getBlockType } = useSelect( blocksStore );
const { getBlockRootClientId, isZoomOut, hasMultiSelection, getBlockName } =
unlock( useSelect( blockEditorStore ) );
const {
insertAfterBlock,
removeBlock,
resetZoomLevel,
startDraggingBlocks,
stopDraggingBlocks,
} = unlock( useDispatch( blockEditorStore ) );

return useRefEffect(
( node ) => {
Expand Down Expand Up @@ -76,7 +83,102 @@ export function useEventHandlers( { clientId, isSelected } ) {
* @param {DragEvent} event Drag event.
*/
function onDragStart( event ) {
event.preventDefault();
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();

const domNode = document.createElement( 'div' );
const root = createRoot( domNode );
root.render(
<BlockDraggableChip
icon={ getBlockType( getBlockName( clientId ) ).icon }
/>
);
document.body.appendChild( domNode );
domNode.style.position = 'absolute';
domNode.style.top = '0';
domNode.style.left = '0';
domNode.style.zIndex = '1000';
domNode.style.pointerEvents = 'none';

// 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 );

let offset = { x: 0, y: 0 };

if ( document !== ownerDocument ) {
const frame = defaultView.frameElement;
if ( frame ) {
const rect = frame.getBoundingClientRect();
offset = { x: rect.left, y: rect.top };
}
}

// chip handle offset
offset.x -= 58;

function over( e ) {
domNode.style.transform = `translate( ${
e.clientX + offset.x
}px, ${ e.clientY + offset.y }px )`;
}

over( event );

function end() {
ownerDocument.removeEventListener( 'dragover', over );
ownerDocument.removeEventListener( 'dragend', end );
domNode.remove();
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 );

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 );
Expand All @@ -91,11 +193,13 @@ export function useEventHandlers( { clientId, isSelected } ) {
clientId,
isSelected,
getBlockRootClientId,
getBlockIndex,
insertAfterBlock,
removeBlock,
isZoomOut,
resetZoomLevel,
hasMultiSelection,
startDraggingBlocks,
stopDraggingBlocks,
]
);
}
4 changes: 4 additions & 0 deletions packages/block-editor/src/components/iframe/content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,9 @@
}
}
}

.wp-block[draggable] {
cursor: grab;
}
}
}
5 changes: 5 additions & 0 deletions packages/block-editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,11 @@ export function RichTextWrapper(
aria-multiline={ ! disableLineBreaks }
aria-readonly={ shouldDisableEditing }
{ ...props }
// Unset draggable (coming from block props) for contentEditable
// elements because it will interfere with multi block selection
// when the contentEditable and draggable elements are the same
// element.
draggable={ undefined }
aria-label={
bindingsLabel || props[ 'aria-label' ] || placeholder
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export default function useBlockDropZone( {
isGroupable,
isZoomOut,
getSectionRootClientId,
getBlockParents,
} = unlock( useSelect( blockEditorStore ) );
const {
showInsertionPoint,
Expand All @@ -358,13 +359,29 @@ export default function useBlockDropZone( {
// So, ensure that the drag state is set when the user drags over a drop zone.
startDragging();
}

const draggedBlockClientIds = getDraggedBlockClientIds();
const targetParents = [
targetRootClientId,
...getBlockParents( targetRootClientId, true ),
];

// Check if the target is within any of the dragged blocks.
const isTargetWithinDraggedBlocks = draggedBlockClientIds.some(
( clientId ) => targetParents.includes( clientId )
);

if ( isTargetWithinDraggedBlocks ) {
return;
}

const allowedBlocks = getAllowedBlocks( targetRootClientId );
const targetBlockName = getBlockNamesByClientId( [
targetRootClientId,
] )[ 0 ];

const draggedBlockNames = getBlockNamesByClientId(
getDraggedBlockClientIds()
draggedBlockClientIds
);
const isBlockDroppingAllowed = isDropTargetValid(
getBlockType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,17 @@ export default function useDragSelection() {
} );
}

let lastMouseDownTarget;

function onMouseDown( { target } ) {
lastMouseDownTarget = target;
}

function onMouseLeave( { buttons, target, relatedTarget } ) {
if ( ! target.contains( lastMouseDownTarget ) ) {
return;
}

// If we're moving into a child element, ignore. We're tracking
// the mouse leaving the element to a parent, no a child.
if ( target.contains( relatedTarget ) ) {
Expand Down Expand Up @@ -141,6 +151,7 @@ export default function useDragSelection() {
}

node.addEventListener( 'mouseout', onMouseLeave );
node.addEventListener( 'mousedown', onMouseDown );

return () => {
node.removeEventListener( 'mouseout', onMouseLeave );
Expand Down
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
- Upgraded `@ariakit/react` (v0.4.13) and `@ariakit/test` (v0.4.5) ([#65907](https://github.com/WordPress/gutenberg/pull/65907)).
- Upgraded `@ariakit/react` (v0.4.15) and `@ariakit/test` (v0.4.7) ([#67404](https://github.com/WordPress/gutenberg/pull/67404)).

### Bug Fixes

- `ResizableBox`: Make drag handles focusable ([#67305](https://github.com/WordPress/gutenberg/pull/67305)).

## 28.13.0 (2024-11-27)

### Deprecations
Expand Down
10 changes: 10 additions & 0 deletions packages/components/src/resizable-box/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ function UnforwardedResizableBox(
showHandle && 'has-show-handle',
className
) }
// Add a focusable element within the drag handle. Unfortunately,
// `re-resizable` does not make them properly focusable by default,
// causing focus to move the the block wrapper which triggers block
// drag.
handleComponent={ Object.fromEntries(
Object.keys( HANDLE_CLASSES ).map( ( key ) => [
key,
<div key={ key } tabIndex={ -1 } />,
] )
) }
handleClasses={ HANDLE_CLASSES }
handleStyles={ HANDLE_STYLES }
ref={ ref }
Expand Down
Loading

0 comments on commit 9ffbd9c

Please sign in to comment.