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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/reference/deprecated.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 2 additions & 5 deletions edit-post/assets/stylesheets/_z-index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions packages/components/src/dashicon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Copy link
Member Author

Choose a reason for hiding this comment

The 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 ) {
Expand Down Expand Up @@ -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>
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/draggable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 );
Expand Down
79 changes: 79 additions & 0 deletions packages/components/src/higher-order/with-draggable/README.md
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 );
```
154 changes: 154 additions & 0 deletions packages/components/src/higher-order/with-draggable/index.js
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(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
20 changes: 20 additions & 0 deletions packages/components/src/higher-order/with-draggable/style.scss
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;
}
1 change: 1 addition & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
31 changes: 0 additions & 31 deletions packages/editor/src/components/block-list/block-draggable.js

This file was deleted.

Loading