Skip to content

Commit

Permalink
Reorder blocks via drag & drop (v2. using editor dropzones). (#4115)
Browse files Browse the repository at this point in the history
* Drag & Drop: Added functionality for reindexing blocks.

- Updated reducers and actions to allow reindexing of blocks given uid of block and new index position.
- Added respective tests.

* Drag & Drop for blocks: Added drag listeners and styling for dragging and reordering blocks.

- Using the current dropzones and onDrop handlers.
- Added background overlay to blocks for using as drag area.
- Added drag handling to block mover so we can grab the block from there.
- Added global styling for changing the cursor to hand.

* Drag & Drop for blocks: Updated drop logic to improve precision of the drop position.

Updated the precision of determining the drop position for a block.
Previously, this was borrowed from the "drop files" logic. This created
inconsistencies due to not accounting that a block is being removed
from its position and reindexed.

* Drag & Drop for blocks: Added z-index to block underlay used in dragging.

* Drag & Drop for blocks: Changed background color of block inset/underlay
used in dragging.

* Drag & Drop for blocks: Fixed visible grayed inset area when dragging.

- Added inner div to underlay container.
- Inner div's area can be adjusted further to desirable size.

* Drag & Drop for blocks: Removed unnecessary block container element.

* Drag & Drop for blocks: Updated cursor when hovering/dragging a
block.

- Set to grab when on hover.
- Set to grabbing when dragging.
- Set the same hover rule for the block mover (the original takes
  precedence when hovering over the arrows).

* Drag & Drop for blocks: Added z-index property to placeholder component in order to prioritise over the inset/underlay used for dragging.

* Drag & Drop for blocks: Updated dragging inset margin to be effective on larger views only.

* Drag & Drop for blocks: Fixed issues that surfaced since merging latest master changes.

- An error surfaced that caused both onDrop and onFilesDrop handlers to fire when
  dragging a block over a drop zone.
- Fixed it by conditioning on the "DataTransfer.effectAllowed" property.
- Set "move" to be the effect used for reordering elements and
  everything else for dropping files.

* Drag & Drop for blocks: Some cleanup - renamed 'underlay' to 'drag-inset', and wrapped in a timeout the resetting of the block display after a drag.

* Drag & Drop for blocks: Changed dataTransfer data type for moving blocks to 'text'.

* Drag & Drop for blocks: Improved conditioning for onDrop handler.

- Added a 'type' flag in the event data transfer.
- Potentially this approach will facilitate multiple onDrop handlers
  for reordering other areas (i.e. images in a gallery - we'd use a
  different constant in that case).

* Drag & Drop for blocks: Added a small margin between cursor and drag
image.

* Drag & Drop for blocks: Removed browser prefix for cursor style.

* Drag & Drop for blocks: IE11 tweaks. Added conditional call to
DataTransfer.setDragImage.

- IE11 does not support this call.
- Dragging/reordering continues to work as expected in IE11 but
  without the custom drag image.
- The gray inset area is also set as expected in IE11.

* Drag & Drop for blocks: Some styling cleanup.

* Drag & Drop for blocks: IE11/Safari tweaks.

- Updated conditioning for the files drop logic to be on availability of
  files in the files list.
  - Neither of the above browsers support the drop effect api for drag &
    drop.
  - Need a way to distinguish between a file upload and a different drop
    effect.

* Drag & Drop for blocks: Updated logic for setting drag image.

- Added a clone node used for customising and setting the drag image.
- Includes a shadow in Chrome and Firefox - no shadow in Safari.
- No drag image for IE11 yet. Same logic however can be extended to drag
  around the clone instead.
- Updated the location of showing the drag image - it will now appear
  right over the block to be reordered (more natural this way).

* Drag & Drop for blocks: Updated drag image logic to be consistent across
Chrome, Firefox, and Safari.

- We create a block clone and spawn it right over the block.
- Set is as drag image and remove it from the DOM right after.
- Behaviour is consistent now with all of the above browsers.

* Drag & Drop for blocks: Updated drag image shadows and styling.

- Shadows dropped only for block footprint.
- On large blocks that span multiple pages we optimise a little by
  setting the top origin of the drag image to be at 0 of viewport (if
  hidden).

* Drag & Drop for blocks: Updated logic for controlling the visibility of
block and inset during a drag operation.

- Now visibility is controlled via a state variable.
- Ensures that the styling is not affected by re-rendering of the block.

* Drag & Drop for blocks: Some cleanup after code review.

* Drag & Drop for blocks: Added more draggable handles to the block, including the ellipsis.

* Drag & Drop for blocks: Some cleanup.

* Drag & Drop for blocks: Updated cursor to move/grab for the block-settings-menu (missed in the previous commit :-)

* Drag & Drop for blocks: Cleanup from previous commit - set the cursor to grabbing by mistake.

* Drag & Drop for blocks: Updated dragging logic to move clone around instead, mostly for IE11 compatibility

* Drag & Drop for blocks: Some cleanup - removed logging, and added a higher z-index to the block clone

* Drag & Drop for blocks: Cleared linting errors.

* Drag & Drop for blocks: Updated drag start/end handlers to stop propagating the events, as they may fire from different layers.

* Drag & Drop for blocks: Added a fake/invisible drag image to avoid
browsers that support the API setDragImage call setting a default cursor
and image + some cleanup and inline comments.

* Drag & Drop for blocks: Added transformation to the block clone if
original block is larger than a fixed height of 700px. The clone is
scaled down by 0.5 and spawned near the cursror instead of over the
original block.

* Drag & Drop for blocks: Some code cleanup.

* Drag & Drop for blocks: Updated block-mover and dropdown components to
avoid rest parameters for the additional drag & drop handlers required
for the drag operation,

* Drag & Drop for blocks: Some cleanup - inline comments, new lines, etc.

* Drag & Drop for blocks: Making sure the DOM is not updated prior to remove clone.

* Drag & Drop for blocks: Moved drag init/end logic to a higher order
component that will be easier to maintain and re-use down the line.

Reasoning:
- Cloning an element and creating a cross-browser drag image to move
  around while dragging does not pertain to blocks only. This can
  potentialy be re-used in other scenarios (extensions may be necessary
  though).
- Since the props required (onDragStart & onDragEnd) are generic wrapper
  methods, a HOC seemed appropriate to encapsulate.

* Drag & Drop for blocks: Cleanup.

* Drag & Drop for blocks: Cleanup. Removed leftover styles.

* Drag & Drop for blocks: Code cleanup.

* Drag & Drop for blocks: Code cleanup. Further extracted drag init/end
logic to outer components directory as a standalone higher order
component.Extracted styles and added README.

* Drag & Drop for blocks: Updated cursor styling to grab for the new block settings menu.

* Drag & Drop for blocks: Updates after PR feedback.

- Removed timeouts where not necessary and update the necessary one
  withSafeTimeout.
- Removed state updates from withDragging HOC and moved config variables
  to module constants.
- Applied HOC directly on block component instead of wrapper layout.

* Drag & Drop for blocks: Some cleanup after PR feedback. removed unnecessary uses of parseInt in withDragging HOC.

* Drag & Drop for blocks: Updated cursor styling after PR feedback to get rid of flickering when moving the cursor across the inset and the mover/settings-menu.

* Drag & Drop for blocks: Updated index of reusable block edit panel to prioritise over underlay inset used for drag listener.

* Drag & Drop for blocks: Some cleanup after PR feedback. Renamed _state variable in reducer to nextSubState..

* Drag & Drop for blocks: Added support for inner blocks.

* Drag & Drop for blocks: Cleanup - lint errors.

* Drag & Drop for blocks: Updates after PR review. No longer passing drag
handlers to the UI components used in a block. Added container wrappers
instead.

* Drag & Drop for blocks: Got rid of BLOCK_REORDER constant and selector.

* Drag & Drop for blocks: Updates and cleanup after PR review.

* Drag & Drop for blocks: Cleanup. Removed commented out code.

* Drag And Drop: Refactor using a component instead of Higher Order Component

* Clarify Drop Events

* Fix dragging between nested and not nested blocks

* Fix Drag Area and Styling

* More cleaning and fix insert position

* Extract BlockDraggable Component

* Destructre the uid block prop

* Clarify BlockDraggable classnames

* Clarify Draggable Component Docs

* Avoid creating a new block object if the layout didn't change when moving the block

* Less generic body className

* Bind BlockDropZone event handlers to avoid rerenderings

And adds a README for the DropZone component

* Remove Block UI on the cloned block element which fixes drag and scroll

* Decrease the opacity of the cloned draggable

* Updating ReactTextareraAutosize to fix a bug on initial load (long post titles)

* Remove iframes from the clone to fix embed's drag and drop

* Fix multiple dropzones showing up in Gallery block

* Changes per review

* Fix scrolling when dragging

* Remove useless styles

* Fix wide blocks scroll

* Fix typos in documentation
  • Loading branch information
chriskmnds authored and youknowriad committed Apr 3, 2018
1 parent 6a0d457 commit c0ee73a
Show file tree
Hide file tree
Showing 20 changed files with 814 additions and 130 deletions.
37 changes: 37 additions & 0 deletions components/draggable/README.md
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`
161 changes: 161 additions & 0 deletions components/draggable/index.js
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 ) {
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 );
20 changes: 20 additions & 0 deletions components/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;
}
47 changes: 47 additions & 0 deletions components/drop-zone/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 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

```jsx
import { DropZoneProvider, DropZone } from '@wordpress/components';

function MyComponent() {
return (
<DropZoneProvider>
<div>
<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`
27 changes: 3 additions & 24 deletions components/drop-zone/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ class DropZone extends Component {
super( ...arguments );

this.setZoneNode = this.setZoneNode.bind( this );
this.onDrop = this.onDrop.bind( this );
this.onFilesDrop = this.onFilesDrop.bind( this );
this.onHTMLDrop = this.onHTMLDrop.bind( this );

this.state = {
isDraggingOverDocument: false,
Expand All @@ -36,34 +33,16 @@ class DropZone extends Component {
this.context.dropzones.add( {
element: this.zone,
updateState: this.setState.bind( this ),
onDrop: this.onDrop,
onFilesDrop: this.onFilesDrop,
onHTMLDrop: this.onHTMLDrop,
onDrop: this.props.onDrop,
onFilesDrop: this.props.onFilesDrop,
onHTMLDrop: this.props.onHTMLDrop,
} );
}

componentWillUnmount() {
this.context.dropzones.remove( this.zone );
}

onDrop() {
if ( this.props.onDrop ) {
this.props.onDrop( ...arguments );
}
}

onFilesDrop() {
if ( this.props.onFilesDrop ) {
this.props.onFilesDrop( ...arguments );
}
}

onHTMLDrop() {
if ( this.props.onHTMLDrop ) {
this.props.onHTMLDrop( ...arguments );
}
}

setZoneNode( node ) {
this.zone = node;
}
Expand Down
Loading

0 comments on commit c0ee73a

Please sign in to comment.