-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Reorder blocks via drag & drop (v2. using editor dropzones). #4115
Changes from 82 commits
b62160a
7723d70
51cd0f5
82e9886
d6df73d
f38db58
1b04b5b
9f25b10
0a23609
9f3b3af
52ac1ff
c55ebc3
6c7a913
8dbc69e
35f9923
9271649
8fdcd6e
1e2ae5e
ec5052c
62afc6d
8f13212
883ab19
6faceef
4978348
b512271
e2c2e61
8436f21
f131740
2599d42
6ad1c44
6fc98de
178cc60
c98ea9c
6c2bf61
ab68b11
702eec3
7cbdfde
b0f0b0e
43ec29a
ec53f4f
2e0b13a
a2246f4
db7a34b
d8bedee
25485f9
05f2d10
f33d4db
a83c738
30ee308
da001c9
b071cc4
46ed7a2
129dac9
d6897ce
4e66a4a
bfb78c1
0a6a05d
f72e05d
f03e4ec
650f613
5c1d339
5c1613d
4d3b8b8
d731edf
9624dca
94dd1cc
7f01121
9982bcd
5623a71
f48d65a
d415a1f
0eca12c
e763e28
f19dcd0
48f8087
2759481
51c1cef
794dfd0
4a817c5
7dcc860
b20b033
c61c770
d5daf30
a648f67
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,37 @@ | ||
# Draggable | ||
|
||
`Draggable` is a Component that can wrap any element to make it draggable. When used, a cross-browser (including IE) customisable drag image is created. The component clones the specified element on drag-start and uses the clone as a drag image during drag-over. Discards the clone on drag-end. | ||
|
||
## Props | ||
|
||
The component accepts the following props: | ||
|
||
### elementId | ||
|
||
The HTML id of the element to clone on drag | ||
|
||
- Type: `string` | ||
- Required: Yes | ||
|
||
### transferData | ||
|
||
Arbitrary data object attached to the drag and drop event. | ||
|
||
- Type: `Object` | ||
- Required: Yes | ||
|
||
### onDragStart | ||
|
||
The function called when dragging starts. | ||
|
||
- Type: `Function` | ||
- Required: No | ||
- Default: `noop` | ||
|
||
### onDragEnd | ||
|
||
The function called when dragging ends. | ||
|
||
- Type: `Function` | ||
- Required: No | ||
- Default: `noop` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { noop } from 'lodash'; | ||
import classnames from 'classnames'; | ||
|
||
/** | ||
* WordPress Dependencies | ||
*/ | ||
import { Component } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal Dependencies | ||
*/ | ||
import withSafeTimeout from '../higher-order/with-safe-timeout'; | ||
import './style.scss'; | ||
|
||
const dragImageClass = 'components-draggable__invisible-drag-image'; | ||
const cloneWrapperClass = 'components-draggable__clone'; | ||
const cloneHeightTransformationBreakpoint = 700; | ||
const clonePadding = 20; | ||
|
||
class Draggable extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
this.onDragStart = this.onDragStart.bind( this ); | ||
this.onDragOver = this.onDragOver.bind( this ); | ||
this.onDragEnd = this.onDragEnd.bind( this ); | ||
} | ||
|
||
componentWillUnmount() { | ||
this.removeDragClone(); | ||
} | ||
|
||
/** | ||
* Removes the element clone, resets cursor, and removes drag listener. | ||
* @param {Object} event The non-custom DragEvent. | ||
*/ | ||
onDragEnd( event ) { | ||
const { onDragEnd = noop } = this.props; | ||
this.removeDragClone(); | ||
// Reset cursor. | ||
document.body.classList.remove( 'is-dragging-components-draggable' ); | ||
event.preventDefault(); | ||
|
||
this.props.setTimeout( onDragEnd ); | ||
} | ||
|
||
/* | ||
* Updates positioning of element clone based on mouse movement during dragging. | ||
* @param {Object} event The non-custom DragEvent. | ||
*/ | ||
onDragOver( event ) { | ||
this.cloneWrapper.style.top = | ||
`${ parseInt( this.cloneWrapper.style.top, 10 ) + event.clientY - this.cursorTop }px`; | ||
this.cloneWrapper.style.left = | ||
`${ parseInt( this.cloneWrapper.style.left, 10 ) + event.clientX - this.cursorLeft }px`; | ||
|
||
// Update cursor coordinates. | ||
this.cursorLeft = event.clientX; | ||
this.cursorTop = event.clientY; | ||
} | ||
|
||
/** | ||
* - Clones the current element and spawns clone over original element. | ||
* - Adds a fake temporary drag image to avoid browser defaults. | ||
* - Sets transfer data. | ||
* - Adds dragover listener. | ||
* @param {Object} event The non-custom DragEvent. | ||
* @param {string} elementId The HTML id of the element to be dragged. | ||
* @param {Object} transferData The data to be set to the event's dataTransfer - to be accessible in any later drop logic. | ||
*/ | ||
onDragStart( event ) { | ||
const { elementId, transferData, onDragStart = noop } = this.props; | ||
const element = document.getElementById( elementId ); | ||
if ( ! element ) { | ||
event.preventDefault(); | ||
return; | ||
} | ||
|
||
// Set a fake drag image to avoid browser defaults. Remove from DOM | ||
// right after. event.dataTransfer.setDragImage is not supported yet in | ||
// IE, we need to check for its existence first. | ||
if ( 'function' === typeof event.dataTransfer.setDragImage ) { | ||
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. Why do we check ...And as a follow-up, should we note via code comment that it's for browser compatibility? 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. Of course - will do. |
||
const dragImage = document.createElement( 'div' ); | ||
dragImage.id = `drag-image-${ elementId }`; | ||
dragImage.classList.add( dragImageClass ); | ||
document.body.appendChild( dragImage ); | ||
event.dataTransfer.setDragImage( dragImage, 0, 0 ); | ||
this.props.setTimeout( () => { | ||
document.body.removeChild( dragImage ); | ||
} ); | ||
} | ||
|
||
event.dataTransfer.setData( 'text', JSON.stringify( transferData ) ); | ||
|
||
// Prepare element clone and append to element wrapper. | ||
const elementRect = element.getBoundingClientRect(); | ||
const elementWrapper = element.parentNode; | ||
const elementTopOffset = parseInt( elementRect.top, 10 ); | ||
const elementLeftOffset = parseInt( elementRect.left, 10 ); | ||
const clone = element.cloneNode( true ); | ||
clone.id = `clone-${ elementId }`; | ||
this.cloneWrapper = document.createElement( 'div' ); | ||
this.cloneWrapper.classList.add( cloneWrapperClass ); | ||
this.cloneWrapper.style.width = `${ elementRect.width + ( clonePadding * 2 ) }px`; | ||
|
||
if ( elementRect.height > cloneHeightTransformationBreakpoint ) { | ||
// Scale down clone if original element is larger than 700px. | ||
this.cloneWrapper.style.transform = 'scale(0.5)'; | ||
this.cloneWrapper.style.transformOrigin = 'top left'; | ||
// Position clone near the cursor. | ||
this.cloneWrapper.style.top = `${ event.clientY - 100 }px`; | ||
this.cloneWrapper.style.left = `${ event.clientX }px`; | ||
} else { | ||
// Position clone right over the original element (20px padding). | ||
this.cloneWrapper.style.top = `${ elementTopOffset - clonePadding }px`; | ||
this.cloneWrapper.style.left = `${ elementLeftOffset - clonePadding }px`; | ||
} | ||
|
||
// Hack: Remove iFrames as it's causing the embeds drag clone to freeze | ||
[ ...clone.querySelectorAll( 'iframe' ) ].forEach( child => child.parentNode.removeChild( child ) ); | ||
|
||
this.cloneWrapper.appendChild( clone ); | ||
elementWrapper.appendChild( this.cloneWrapper ); | ||
|
||
// Mark the current cursor coordinates. | ||
this.cursorLeft = event.clientX; | ||
this.cursorTop = event.clientY; | ||
// Update cursor to 'grabbing', document wide. | ||
document.body.classList.add( 'is-dragging-components-draggable' ); | ||
document.addEventListener( 'dragover', this.onDragOver ); | ||
|
||
this.props.setTimeout( onDragStart ); | ||
} | ||
|
||
removeDragClone() { | ||
document.removeEventListener( 'dragover', this.onDragOver ); | ||
if ( this.cloneWrapper && this.cloneWrapper.parentNode ) { | ||
// Remove clone. | ||
this.cloneWrapper.parentNode.removeChild( this.cloneWrapper ); | ||
this.cloneWrapper = null; | ||
} | ||
} | ||
|
||
render() { | ||
const { children, className } = this.props; | ||
return ( | ||
<div | ||
className={ classnames( 'components-draggable', className ) } | ||
onDragStart={ this.onDragStart } | ||
onDragEnd={ this.onDragEnd } | ||
draggable | ||
> | ||
{ children } | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
export default withSafeTimeout( Draggable ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
body.is-dragging-components-draggable { | ||
cursor: move;/* Fallback for IE/Edge < 14 */ | ||
cursor: grabbing !important; | ||
} | ||
|
||
.components-draggable__invisible-drag-image { | ||
position: fixed; | ||
left: -1000px; | ||
height: 50px; | ||
width: 50px; | ||
} | ||
|
||
.components-draggable__clone { | ||
position: fixed; | ||
padding: 20px; | ||
background: transparent; | ||
pointer-events: none; | ||
z-index: z-index( '.components-draggable__clone' ); | ||
opacity: 0.8; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# DropZone | ||
|
||
`DropZone` is a Component creating a drop zone area taking the full size of its parent element. It supports dropping files, HTML content or any other HTML drop event. To work properly this components needs to be wrapped in a `DropZoneProvider`. | ||
|
||
## Usage | ||
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. Duplicate heading. |
||
|
||
|
||
## Usage | ||
|
||
```jsx | ||
import { DropZoneProvider, DropZone } from '@wordpress/components'; | ||
|
||
function MyComponent() { | ||
return ( | ||
<DropZoneProvider> | ||
<div> | ||
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. Is the intermediate |
||
<DropZone onDrop={ () => console.log( 'do something' ) } /> | ||
</div> | ||
</DropZoneProvider> | ||
); | ||
} | ||
``` | ||
|
||
## Props | ||
|
||
The component accepts the following props: | ||
|
||
### onFilesDrop | ||
|
||
The function is called when dropping a file into the `DropZone`. It receives two arguments: an array of dropped files and a position object which the following shape: `{ x: 'left|right', y: 'top|bottom' }`. The position object indicates whether the drop event happened closer to the top or bottom edges and left or right ones. | ||
|
||
- Type: `Function` | ||
- Required: No | ||
- Default: `noop` | ||
|
||
### onHTMLDrop | ||
|
||
The function is called when dropping a file into the `DropZone`. It receives two arguments: the HTML being dropped and a position object. | ||
|
||
- Type: `Function` | ||
- Required: No | ||
- Default: `noop` | ||
|
||
### onDrop | ||
|
||
The function is generic drop handler called if the `onFilesDrop` or `onHTMLDrop` are not called. It receives two arguments: The drop `event` object and the position object. | ||
|
||
- Type: `Function` | ||
- Required: No | ||
- Default: `noop` |
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.
Normally, in React, we'd use
ref
for this sort of thing. I understand that BlockListBlock is a bit different in that BlockDraggable is nested in the element itself via IgnoreNestedEvents, thus making it trickier to grab and pass the right ref. Edit: Actually, passing the ID string may make this component more easily reusable 👍.If we stick to passing
elementId
, which seems fine, should we make sure thatelement
exists?