Skip to content
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

Closed
wants to merge 21 commits into from
Closed
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.
+307 −134
Diff settings

Always

Just for now

@@ -7,6 +7,7 @@ Gutenberg's deprecation policy is intended to support backwards-compatibility fo

## 3.7.0

- `wp.components.Draggable` has been deprecated. Please, use `wp.components.withDraggable` instead.
- `wp.components.withAPIData` has been removed. Please use the Core Data module or `wp.apiFetch` directly instead.
- `wp.data.dispatch("core").receiveTerms` has been deprecated. Please use `wp.data.dispatch("core").receiveEntityRecords` instead.
- `getCategories` resolvers has been deprecated. Please use `getEntityRecords` resolver instead.
@@ -9,7 +9,7 @@ $z-layers: (
".editor-block-list__block {core/image aligned left or right}": 20,
".editor-block-list__block {core/image aligned wide or fullwide}": 20,
".freeform-toolbar": 10,
".editor-block-list__breadcrumb": 1,
".editor-block-list__breadcrumb": 100, // drag handler, to be shown below any overlay
".components-form-toggle__input": 1,
".editor-inserter__tabs": 1,
".editor-inserter__tab.is-active": 1,
@@ -34,10 +34,7 @@ $z-layers: (
// Active pill button
".components-button.is-button {:focus or .is-primary}": 1,

// Should have lower index than anything else positioned inside the block containers
".editor-block-list__block-draggable": 0,

// The draggable element should show up above the entire UI
// The drag image should show up above the entire UI
".components-draggable__clone": 1000000000,

// Should have higher index than the inset/underlay used for dragging
@@ -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 ||

This comment has been minimized.

Copy link
@nosolosw

nosolosw Aug 17, 2018

Author Member

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>
@@ -9,6 +9,7 @@ import classnames from 'classnames';
*/
import { Component } from '@wordpress/element';
import { withSafeTimeout } from '@wordpress/compose';
import deprecated from '@wordpress/deprecated';

const dragImageClass = 'components-draggable__invisible-drag-image';
const cloneWrapperClass = 'components-draggable__clone';
@@ -19,6 +20,11 @@ class Draggable extends Component {
constructor() {
super( ...arguments );

deprecated( 'wp.components.Draggable', {
version: 3.7,
alternative: 'wp.components.withDraggable',
} );

this.onDragStart = this.onDragStart.bind( this );
this.onDragOver = this.onDragOver.bind( this );
this.onDragEnd = this.onDragEnd.bind( this );
@@ -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 );
```
@@ -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(

This comment has been minimized.

Copy link
@nosolosw

nosolosw Aug 21, 2018

Author Member

FWIW, this component ports the existing Draggable functionality (set data transfer and create a drag image).

( 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;
@@ -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;
}
@@ -62,6 +62,7 @@ export { default as navigateRegions } from './higher-order/navigate-regions';
export { default as withAPIData } from './higher-order/with-api-data';
export { default as withContext } from './higher-order/with-context';
export { default as withConstrainedTabbing } from './higher-order/with-constrained-tabbing';
export { default as withDraggable } from './higher-order/with-draggable';
export { default as withFallbackStyles } from './higher-order/with-fallback-styles';
export { default as withFilters } from './higher-order/with-filters';
export { default as withFocusOutside } from './higher-order/with-focus-outside';

This file was deleted.

Oops, something went wrong.
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.