-
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
Use the breadcrumb as the draggable handle #8764
Changes from 20 commits
add68b9
d4f6fa7
d06670f
c119acb
32a465f
97f17ea
9c694b6
123d37b
a37bcde
fcd43b7
ebfe05a
ca3dbf6
4cea178
47f02fe
74d1b9c
77f7ef1
26ca9bf
ac43bb3
7c37b06
9d4793a
9999a4f
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 |
---|---|---|
|
@@ -15,12 +15,13 @@ export default class Dashicon extends Component { | |
return ( | ||
this.props.icon !== nextProps.icon || | ||
this.props.size !== nextProps.size || | ||
this.props.viewBox !== nextProps.viewBox || | ||
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. These changes to the dashicon component is an interim hack. We need to either port them to upstream dashicon repo if they make sense or figure out a different approach to show the drag affordance to the user. |
||
this.props.className !== nextProps.className | ||
); | ||
} | ||
|
||
render() { | ||
const { icon, className, size = 20 } = this.props; | ||
const { icon, className, size = 20, viewBox = 20 } = this.props; | ||
let path; | ||
|
||
switch ( icon ) { | ||
|
@@ -893,7 +894,7 @@ export default class Dashicon extends Component { | |
xmlns="http://www.w3.org/2000/svg" | ||
width={ size } | ||
height={ size } | ||
viewBox="0 0 20 20" | ||
viewBox={ '0 0 ' + viewBox + ' ' + viewBox } // TODO: if we like this approach, remove this hack and implement upstream | ||
> | ||
<path d={ path } /> | ||
</svg> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# withDraggable | ||
|
||
`withDraggable` is a Higher-Order Component that provides a way to set up a cross-browser (including IE) customisable drag image, and the transfer data for the drag event. It decouples the drag handle and the element to drag: it wraps the component that will be the drag handle, and it should be provided the DOM ID of the element to drag. | ||
|
||
Note that the drag handle needs to declare the `draggable="true"` property. The `withDraggable` component only takes care of the logic to setup the drag image and the transfer data, but is not concerned with creating an actual DOM element that is draggable. | ||
|
||
## Props | ||
|
||
The component injects the following props into the wrapped component: | ||
|
||
### initDragging | ||
|
||
A function that initializes the drag event, setting up the transfer data and creating the drag image. It returns a function to be called on the `dragstart` DOM event. | ||
|
||
- Type: `Function` | ||
- Required: Yes | ||
- Arguments: | ||
- `elementId`: DOM id of the element to be dragged | ||
- `data`: the [data](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/dataTransfer) to be transfered by the event | ||
|
||
## Usage | ||
|
||
```jsx | ||
import { Dashicon, Panel, PanelBody, withDraggable } from '@wordpress/components'; | ||
|
||
const MyDraggable = ( { initDragging } ) => ( | ||
<div id="draggable-panel"> | ||
<Panel header="Draggable panel" > | ||
<PanelBody> | ||
<Dashicon | ||
icon="move" | ||
draggable="true" | ||
onDragStart={ initDragging( "draggable-panel", {} ) } /> | ||
</PanelBody> | ||
</Panel> | ||
</div> | ||
); | ||
|
||
export default withDraggable( MyDraggable ); | ||
``` | ||
|
||
If the wrapped element would want to inject their own logic into the `dragstart` event, it should initialize the `initDragging` prop provided by `withDraggable` in that event handler. For example: | ||
|
||
```jsx | ||
|
||
import { Dashicon, Panel, PanelBody, withDraggable } from '@wordpress/components'; | ||
|
||
const class MyDraggable extends Component { | ||
|
||
constructor() { | ||
super( ...arguments ); | ||
this.myDragStart = this.myDragStart.bind( this ); | ||
} | ||
|
||
myDragStart( event ){ | ||
this.props.initDragging( 'draggable-panel', {} )( event ); | ||
|
||
// can do whatever is necessary after the event has been set up | ||
} | ||
|
||
render( ) { | ||
return ( | ||
<div id="draggable-panel"> | ||
<Panel header="Draggable panel" > | ||
<PanelBody> | ||
<Dashicon | ||
icon="move" | ||
draggable="true" | ||
onDragStart={ myDragStart } | ||
/> | ||
</PanelBody> | ||
</Panel> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
export default withDraggable( MyDraggable ); | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
/** | ||
* WordPress Dependencies | ||
*/ | ||
import { Component } from '@wordpress/element'; | ||
import { createHigherOrderComponent } from '@wordpress/compose'; | ||
|
||
const dragImageClass = 'components-draggable__invisible-drag-image'; | ||
const cloneWrapperClass = 'components-draggable__clone'; | ||
const cloneHeightTransformationBreakpoint = 700; | ||
const clonePadding = 20; | ||
|
||
const withDraggable = createHigherOrderComponent( | ||
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. FWIW, this component ports the existing |
||
( OriginalComponent ) => { | ||
return class extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
this.onDragStart = this.onDragStart.bind( this ); | ||
this.onDragOver = this.onDragOver.bind( this ); | ||
this.onDragEnd = this.onDragEnd.bind( this ); | ||
this.resetDragState = this.resetDragState.bind( this ); | ||
} | ||
|
||
componentWillUnmount() { | ||
this.resetDragState(); | ||
} | ||
|
||
/** | ||
* Function that creates the dragstart event handler. | ||
* | ||
* @param {string} elementId The HTML id of the element to be dragged. | ||
* @param {Object} data The data to be set to the event's dataTransfer - to be accessible in any later drop logic. | ||
* @return {function} A function for wrapped components to use as their onDragStart handler. | ||
*/ | ||
onDragStart( elementId, data ) { | ||
return ( event ) => { | ||
const element = document.getElementById( elementId ); | ||
if ( ! element || ! data ) { | ||
event.preventDefault(); | ||
return; | ||
} | ||
|
||
event.dataTransfer.setData( 'text', JSON.stringify( data ) ); | ||
|
||
// 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 ) { | ||
const dragImage = document.createElement( 'div' ); | ||
dragImage.id = `drag-image-${ elementId }`; | ||
dragImage.classList.add( dragImageClass ); | ||
document.body.appendChild( dragImage ); | ||
event.dataTransfer.setDragImage( dragImage, 0, 0 ); | ||
setTimeout( () => { | ||
document.body.removeChild( dragImage ); | ||
} ); | ||
} | ||
|
||
// 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' ); | ||
|
||
// connect listeners | ||
document.addEventListener( 'dragover', this.onDragOver ); | ||
document.addEventListener( 'dragend', this.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; | ||
} | ||
|
||
/** | ||
* Removes the element clone, resets cursor, and removes drag listener. | ||
*/ | ||
onDragEnd( ) { | ||
this.resetDragState(); | ||
} | ||
|
||
/** | ||
* Cleans up drag state when drag has completed, or component unmounts | ||
* while dragging. | ||
*/ | ||
resetDragState() { | ||
// Remove listeners | ||
document.removeEventListener( 'dragover', this.onDragOver ); | ||
document.removeEventListener( 'dragend', this.onDragEnd ); | ||
|
||
// Remove drag clone | ||
if ( this.cloneWrapper && this.cloneWrapper.parentNode ) { | ||
this.cloneWrapper.parentNode.removeChild( this.cloneWrapper ); | ||
this.cloneWrapper = null; | ||
} | ||
|
||
// Reset cursor. | ||
document.body.classList.remove( 'is-dragging-components-draggable' ); | ||
} | ||
|
||
render() { | ||
return ( | ||
<OriginalComponent | ||
initDragging={ this.onDragStart } | ||
{ ...this.props } | ||
/> | ||
); | ||
} | ||
}; | ||
}, | ||
'withDraggable' | ||
); | ||
|
||
export default withDraggable; |
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; | ||
} |
This file was deleted.
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.
Adjust this number to avoid conflicts with popovers:
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.
And with the classic 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.
9999a4f fixes the problem with the popover.