-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Grid interactivity: Experimenting with drag and drop to set column start and row start #59490
Changes from 31 commits
0bd9683
4be75af
8d0160a
59c3994
81f6936
ed4483a
b8e9a33
2fb3f1b
f68c715
402eddc
8eaa61a
577a7eb
bfe3042
e62f090
f6a9e0a
7dcc874
81463b4
8fb6fd6
c9638ec
5f4aaeb
1726d59
0496060
3ff1f86
8c1e9ee
c3d1c1f
ff98a89
665684b
9d5940f
2a3e588
e675646
d63b85b
073c281
ac5f050
05b0392
b70baee
05900ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __ } from '@wordpress/i18n'; | ||
import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; | ||
import { pin as pinIcon } from '@wordpress/icons'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import BlockControls from '../block-controls'; | ||
import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; | ||
import { getGridItemRect } from './utils'; | ||
|
||
export function GridItemPinToolbarItem( { clientId, layout, onChange } ) { | ||
const blockElement = useBlockElement( clientId ); | ||
if ( ! blockElement ) { | ||
return null; | ||
} | ||
|
||
const isPinned = !! layout?.columnStart || !! layout?.rowStart; | ||
|
||
function unpinBlock() { | ||
onChange( { | ||
columnStart: undefined, | ||
rowStart: undefined, | ||
} ); | ||
} | ||
|
||
function pinBlock() { | ||
noisysocks marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const rect = getGridItemRect( blockElement ); | ||
onChange( { | ||
columnStart: rect.columnStart, | ||
rowStart: rect.rowStart, | ||
} ); | ||
} | ||
|
||
return ( | ||
<BlockControls group="parent"> | ||
<ToolbarGroup> | ||
<ToolbarButton | ||
icon={ pinIcon } | ||
label={ | ||
isPinned ? __( 'Pinned to grid' ) : __( 'Pin to grid' ) | ||
} | ||
isPressed={ isPinned } | ||
onClick={ isPinned ? unpinBlock : pinBlock } | ||
/> | ||
</ToolbarGroup> | ||
</BlockControls> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { ResizableBox } from '@wordpress/components'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; | ||
import BlockPopoverCover from '../block-popover/cover'; | ||
import { getGridRect } from './utils'; | ||
|
||
export function GridItemResizer( { clientId, onChange } ) { | ||
const blockElement = useBlockElement( clientId ); | ||
if ( ! blockElement ) { | ||
return null; | ||
} | ||
return ( | ||
<BlockPopoverCover | ||
className="block-editor-grid-item-resizer" | ||
clientId={ clientId } | ||
__unstablePopoverSlot="block-toolbar" | ||
> | ||
<ResizableBox | ||
className="block-editor-grid-item-resizer__box" | ||
size={ { | ||
width: '100%', | ||
height: '100%', | ||
} } | ||
enable={ { | ||
bottom: true, | ||
bottomLeft: false, | ||
bottomRight: false, | ||
left: false, | ||
right: true, | ||
top: false, | ||
topLeft: false, | ||
topRight: false, | ||
} } | ||
onResizeStop={ ( event, direction, boxElement ) => { | ||
const rect = getGridRect( | ||
blockElement.parentElement, | ||
new window.DOMRect( | ||
blockElement.offsetLeft, | ||
blockElement.offsetTop, | ||
boxElement.offsetWidth, | ||
boxElement.offsetHeight | ||
) | ||
); | ||
onChange( { | ||
columnSpan: rect.columnSpan, | ||
rowSpan: rect.rowSpan, | ||
} ); | ||
} } | ||
/> | ||
</BlockPopoverCover> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import classnames from 'classnames'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useState, useEffect } from '@wordpress/element'; | ||
import { useSelect, useDispatch } from '@wordpress/data'; | ||
import { __experimentalUseDropZone as useDropZone } from '@wordpress/compose'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; | ||
import BlockPopoverCover from '../block-popover/cover'; | ||
import { getComputedCSS, range, GridRect, getGridItemRect } from './utils'; | ||
import { store as blockEditorStore } from '../../store'; | ||
|
||
export function GridVisualizer( { clientId } ) { | ||
const gridElement = useBlockElement( clientId ); | ||
if ( ! gridElement ) { | ||
return null; | ||
} | ||
return ( | ||
<GridVisualizerGrid clientId={ clientId } gridElement={ gridElement } /> | ||
); | ||
} | ||
|
||
function getGridInfo( gridElement ) { | ||
const gridTemplateColumns = getComputedCSS( | ||
gridElement, | ||
'grid-template-columns' | ||
); | ||
const gridTemplateRows = getComputedCSS( | ||
gridElement, | ||
'grid-template-rows' | ||
); | ||
const numColumns = gridTemplateColumns.split( ' ' ).length; | ||
const numRows = gridTemplateRows.split( ' ' ).length; | ||
const numItems = numColumns * numRows; | ||
return { | ||
numColumns, | ||
numRows, | ||
numItems, | ||
style: { | ||
gridTemplateColumns, | ||
gridTemplateRows, | ||
gap: getComputedCSS( gridElement, 'gap' ), | ||
padding: getComputedCSS( gridElement, 'padding' ), | ||
}, | ||
}; | ||
} | ||
|
||
function GridVisualizerGrid( { clientId, gridElement } ) { | ||
const [ gridInfo, setGridInfo ] = useState( () => | ||
getGridInfo( gridElement ) | ||
); | ||
const [ isDroppingAllowed, setIsDroppingAllowed ] = useState( false ); | ||
const [ highlightedRect, setHighlightedRect ] = useState( null ); | ||
|
||
const { getBlockAttributes } = useSelect( blockEditorStore ); | ||
const { updateBlockAttributes } = useDispatch( blockEditorStore ); | ||
|
||
useEffect( () => { | ||
const observers = []; | ||
for ( const element of [ gridElement, ...gridElement.children ] ) { | ||
const observer = new window.ResizeObserver( () => { | ||
setGridInfo( getGridInfo( gridElement ) ); | ||
} ); | ||
observer.observe( element ); | ||
observers.push( observer ); | ||
} | ||
return () => { | ||
for ( const observer of observers ) { | ||
observer.disconnect(); | ||
} | ||
}; | ||
}, [ gridElement ] ); | ||
|
||
useEffect( () => { | ||
function onGlobalDrag() { | ||
setIsDroppingAllowed( true ); | ||
} | ||
function onGlobalDragEnd() { | ||
setIsDroppingAllowed( false ); | ||
} | ||
document.addEventListener( 'drag', onGlobalDrag ); | ||
document.addEventListener( 'dragend', onGlobalDragEnd ); | ||
return () => { | ||
document.removeEventListener( 'drag', onGlobalDrag ); | ||
document.removeEventListener( 'dragend', onGlobalDragEnd ); | ||
}; | ||
}, [] ); | ||
|
||
return ( | ||
<BlockPopoverCover | ||
className={ classnames( 'block-editor-grid-visualizer', { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Somewhere a little 2024-03-15.12.09.46.mp4There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm. Why is this happening? When I look in the inspector the visualiser has There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect they're in different stacking contexts? Or a new one needs to be created around the grid visualizer. This tool is pretty handy to work it out: https://github.com/gwwar/z-context There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah they're in different stacking contexts but what's causing the issue is the popover content moving to the bottom of the DOM when the viewport is smaller than 782px: I'm not sure what the role of |
||
'is-dropping-allowed': isDroppingAllowed, | ||
} ) } | ||
clientId={ clientId } | ||
__unstablePopoverSlot="block-toolbar" | ||
> | ||
<div | ||
className="block-editor-grid-visualizer__grid" | ||
style={ gridInfo.style } | ||
> | ||
{ range( 1, gridInfo.numRows ).map( ( row ) => | ||
range( 1, gridInfo.numColumns ).map( ( column ) => ( | ||
<GridVisualizerCell | ||
key={ `${ row }-${ column }` } | ||
isHighlighted={ | ||
highlightedRect?.contains( column, row ) ?? | ||
false | ||
} | ||
validateDrag={ ( srcClientId ) => { | ||
const attributes = | ||
getBlockAttributes( srcClientId ); | ||
const rect = new GridRect( { | ||
columnStart: column, | ||
rowStart: row, | ||
columnSpan: | ||
attributes.style?.layout?.columnSpan, | ||
rowSpan: attributes.style?.layout?.rowSpan, | ||
} ); | ||
|
||
const isInBounds = new GridRect( { | ||
columnSpan: gridInfo.numColumns, | ||
rowSpan: gridInfo.numRows, | ||
} ).containsRect( rect ); | ||
if ( ! isInBounds ) { | ||
return false; | ||
} | ||
|
||
const isOverlapping = Array.from( | ||
gridElement.children | ||
).some( | ||
( child ) => | ||
child.dataset.block !== srcClientId && | ||
rect.intersectsRect( | ||
getGridItemRect( child ) | ||
) | ||
); | ||
if ( isOverlapping ) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} } | ||
onDragEnter={ ( srcClientId ) => { | ||
const attributes = | ||
getBlockAttributes( srcClientId ); | ||
setHighlightedRect( | ||
new GridRect( { | ||
columnStart: column, | ||
rowStart: row, | ||
columnSpan: | ||
attributes.style?.layout | ||
?.columnSpan, | ||
rowSpan: | ||
attributes.style?.layout?.rowSpan, | ||
} ) | ||
); | ||
} } | ||
onDragLeave={ () => { | ||
// onDragEnter can be called before onDragLeave if the user moves | ||
// their mouse quickly, so only clear the highlight if it was set | ||
// by this cell. | ||
setHighlightedRect( ( prevHighlightedRect ) => | ||
prevHighlightedRect?.columnStart === | ||
column && | ||
prevHighlightedRect?.rowStart === row | ||
? null | ||
: prevHighlightedRect | ||
); | ||
} } | ||
onDrop={ ( srcClientId ) => { | ||
const attributes = | ||
getBlockAttributes( srcClientId ); | ||
updateBlockAttributes( srcClientId, { | ||
style: { | ||
...attributes.style, | ||
layout: { | ||
...attributes.style?.layout, | ||
columnStart: column, | ||
rowStart: row, | ||
}, | ||
}, | ||
} ); | ||
setHighlightedRect( null ); | ||
} } | ||
/> | ||
) ) | ||
) } | ||
</div> | ||
</BlockPopoverCover> | ||
); | ||
} | ||
|
||
function GridVisualizerCell( { | ||
isHighlighted, | ||
validateDrag, | ||
onDragEnter, | ||
onDragLeave, | ||
onDrop, | ||
} ) { | ||
const { getDraggedBlockClientIds } = useSelect( blockEditorStore ); | ||
|
||
const ref = useDropZone( { | ||
onDragEnter() { | ||
const [ srcClientId ] = getDraggedBlockClientIds(); | ||
if ( srcClientId && validateDrag( srcClientId ) ) { | ||
onDragEnter( srcClientId ); | ||
} | ||
}, | ||
onDragLeave() { | ||
onDragLeave(); | ||
}, | ||
onDrop() { | ||
const [ srcClientId ] = getDraggedBlockClientIds(); | ||
if ( srcClientId && validateDrag( srcClientId ) ) { | ||
onDrop( srcClientId ); | ||
} | ||
}, | ||
} ); | ||
|
||
return ( | ||
<div className="block-editor-grid-visualizer__cell"> | ||
<div | ||
ref={ ref } | ||
className={ classnames( | ||
'block-editor-grid-visualizer__drop-zone', | ||
{ | ||
'is-highlighted': isHighlighted, | ||
} | ||
) } | ||
/> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export { GridVisualizer } from './grid-visualizer'; | ||
export { GridItemResizer } from './grid-item-resizer'; | ||
export { GridItemPinToolbarItem } from './grid-item-pin-toolbar-item'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm seeing a
Warning: Received NaN for the
maxattribute.
which I think is coming from here;columnCount
doesn't always exist so this might be NaN on auto layouts.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still seeing this error here on Auto layouts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correction: I also see it in Manual mode when clicking "Pin to grid" without moving the block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. (I think.)