-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Grid interactivity: Experimenting with drag and drop to set column start and row start #59490
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
Changes from 14 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,66 @@ | ||
/** | ||
* 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 { getComputedCSS, getGridLines, getClosestLine } 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 gridElement = blockElement.parentElement; | ||
const columnGap = parseFloat( | ||
getComputedCSS( gridElement, 'column-gap' ) | ||
); | ||
const rowGap = parseFloat( getComputedCSS( gridElement, 'row-gap' ) ); | ||
const gridColumnLines = getGridLines( | ||
getComputedCSS( gridElement, 'grid-template-columns' ), | ||
columnGap | ||
); | ||
const gridRowLines = getGridLines( | ||
getComputedCSS( gridElement, 'grid-template-rows' ), | ||
rowGap | ||
); | ||
const columnStart = | ||
getClosestLine( gridColumnLines, blockElement.offsetLeft ) + 1; | ||
const rowStart = | ||
getClosestLine( gridRowLines, blockElement.offsetTop ) + 1; | ||
onChange( { | ||
columnStart, | ||
rowStart, | ||
} ); | ||
} | ||
|
||
return ( | ||
<BlockControls group="parent"> | ||
<ToolbarGroup> | ||
<ToolbarButton | ||
icon={ pinIcon } | ||
label={ __( 'Pin to grid' ) } | ||
noisysocks marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
isPressed={ isPinned } | ||
onClick={ isPinned ? unpinBlock : pinBlock } | ||
/> | ||
</ToolbarGroup> | ||
</BlockControls> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
/** | ||
* 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 } from './utils'; | ||
import { parseDropEvent } from '../use-on-block-drop'; | ||
import { store as blockEditorStore } from '../../store'; | ||
|
||
export function GridVisualizer( { clientId } ) { | ||
const blockElement = useBlockElement( clientId ); | ||
if ( ! blockElement ) { | ||
return null; | ||
} | ||
return ( | ||
<GridVisualizerGrid | ||
clientId={ clientId } | ||
blockElement={ blockElement } | ||
/> | ||
); | ||
} | ||
|
||
function getGridInfo( blockElement ) { | ||
const gridTemplateColumns = getComputedCSS( | ||
blockElement, | ||
'grid-template-columns' | ||
); | ||
const gridTemplateRows = getComputedCSS( | ||
blockElement, | ||
'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( blockElement, 'gap' ), | ||
padding: getComputedCSS( blockElement, 'padding' ), | ||
}, | ||
}; | ||
} | ||
|
||
function range( start, length ) { | ||
return Array.from( { length }, ( _, i ) => start + i ); | ||
} | ||
|
||
function GridVisualizerGrid( { clientId, blockElement } ) { | ||
const [ gridInfo, setGridInfo ] = useState( () => | ||
getGridInfo( blockElement ) | ||
|
||
); | ||
const [ hasCellPointerEvents, setHasCellPointerEvents ] = useState( false ); | ||
|
||
useEffect( () => { | ||
const observers = []; | ||
for ( const element of [ blockElement, ...blockElement.children ] ) { | ||
const observer = new window.ResizeObserver( () => { | ||
setGridInfo( getGridInfo( blockElement ) ); | ||
} ); | ||
observer.observe( element ); | ||
observers.push( observer ); | ||
} | ||
return () => { | ||
for ( const observer of observers ) { | ||
observer.disconnect(); | ||
} | ||
}; | ||
}, [ blockElement ] ); | ||
|
||
useEffect( () => { | ||
function onGlobalDrag() { | ||
setHasCellPointerEvents( true ); | ||
} | ||
function onGlobalDragEnd() { | ||
setHasCellPointerEvents( 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', { | ||
|
||
'has-cell-pointer-events': hasCellPointerEvents, | ||
} ) } | ||
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 }` } | ||
column={ column } | ||
row={ row } | ||
/> | ||
) ) | ||
) } | ||
</div> | ||
</BlockPopoverCover> | ||
); | ||
} | ||
|
||
function GridVisualizerCell( { column, row } ) { | ||
const [ isDraggingOver, setIsDraggingOver ] = useState( false ); | ||
const { getBlockAttributes } = useSelect( blockEditorStore ); | ||
const { updateBlockAttributes } = useDispatch( blockEditorStore ); | ||
|
||
const ref = useDropZone( { | ||
onDragEnter() { | ||
setIsDraggingOver( true ); | ||
}, | ||
onDragLeave() { | ||
setIsDraggingOver( false ); | ||
}, | ||
onDrop( event ) { | ||
setIsDraggingOver( false ); | ||
const { | ||
srcClientIds: [ srcClientId ], | ||
} = parseDropEvent( event ); | ||
if ( ! srcClientId ) { | ||
return; | ||
} | ||
const attributes = getBlockAttributes( srcClientId ); | ||
updateBlockAttributes( srcClientId, { | ||
style: { | ||
...attributes.style, | ||
layout: { | ||
...attributes.style?.layout, | ||
columnStart: column, | ||
rowStart: row, | ||
}, | ||
}, | ||
} ); | ||
}, | ||
} ); | ||
|
||
return ( | ||
<div | ||
ref={ ref } | ||
className={ classnames( 'block-editor-grid-visualizer__cell', { | ||
'is-dragging-over': isDraggingOver, | ||
} ) } | ||
/> | ||
); | ||
} |
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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
.block-editor-grid-visualizer { | ||
// Specificity to override the z-index and pointer-events set by .components-popover. | ||
&.block-editor-grid-visualizer.block-editor-grid-visualizer { | ||
z-index: z-index(".block-editor-grid-visualizer"); | ||
|
||
.components-popover__content * { | ||
pointer-events: none; | ||
} | ||
|
||
&.has-cell-pointer-events { | ||
.block-editor-grid-visualizer__cell { | ||
pointer-events: all; | ||
} | ||
} | ||
} | ||
} | ||
|
||
.block-editor-grid-visualizer__grid { | ||
display: grid; | ||
} | ||
|
||
.block-editor-grid-visualizer__cell { | ||
border: $border-width dashed $gray-300; | ||
position: relative; | ||
|
||
&.is-dragging-over { | ||
background: var(--wp-admin-theme-color); | ||
} | ||
} | ||
|
||
.block-editor-grid-item-resizer { | ||
// Specificity to override the z-index and pointer-events set by .components-popover. | ||
&.block-editor-grid-item-resizer.block-editor-grid-item-resizer { | ||
z-index: z-index(".block-editor-grid-visualizer"); | ||
|
||
.components-popover__content * { | ||
pointer-events: none; | ||
} | ||
} | ||
} | ||
|
||
.block-editor-grid-item-resizer__box { | ||
border: $border-width solid var(--wp-admin-theme-color); | ||
|
||
.components-resizable-box__handle { | ||
// Specificity to override the pointer-events set by .components-popover. | ||
&.components-resizable-box__handle.components-resizable-box__handle { | ||
pointer-events: all; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
export function getComputedCSS( element, property ) { | ||
return element.ownerDocument.defaultView | ||
.getComputedStyle( element ) | ||
.getPropertyValue( property ); | ||
} | ||
|
||
export function getGridLines( template, gap ) { | ||
const lines = [ 0 ]; | ||
for ( const size of template.split( ' ' ) ) { | ||
const line = parseFloat( size ); | ||
lines.push( lines[ lines.length - 1 ] + line + gap ); | ||
} | ||
return lines; | ||
} | ||
|
||
export function getClosestLine( lines, position ) { | ||
return lines.reduce( | ||
( closest, line, index ) => | ||
Math.abs( line - position ) < | ||
Math.abs( lines[ closest ] - position ) | ||
? index | ||
: closest, | ||
0 | ||
); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.