diff --git a/docs/reference/deprecated.md b/docs/reference/deprecated.md index 0f8b1dabe43fc2..115161f0762c7f 100644 --- a/docs/reference/deprecated.md +++ b/docs/reference/deprecated.md @@ -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. diff --git a/edit-post/assets/stylesheets/_z-index.scss b/edit-post/assets/stylesheets/_z-index.scss index 9a350e6868e7f6..c560caf2bfc97f 100644 --- a/edit-post/assets/stylesheets/_z-index.scss +++ b/edit-post/assets/stylesheets/_z-index.scss @@ -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 diff --git a/packages/components/src/dashicon/index.js b/packages/components/src/dashicon/index.js index c103795736e638..4541f1dc56ecfd 100644 --- a/packages/components/src/dashicon/index.js +++ b/packages/components/src/dashicon/index.js @@ -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.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 > diff --git a/packages/components/src/draggable/index.js b/packages/components/src/draggable/index.js index f6c85d6d40834b..63e2e50b5bc175 100644 --- a/packages/components/src/draggable/index.js +++ b/packages/components/src/draggable/index.js @@ -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 ); diff --git a/packages/components/src/higher-order/with-draggable/README.md b/packages/components/src/higher-order/with-draggable/README.md new file mode 100644 index 00000000000000..4b12a4afca90ab --- /dev/null +++ b/packages/components/src/higher-order/with-draggable/README.md @@ -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 } ) => ( +
+ + + + + +
+); + +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 ( +
+ + + + + +
+ ); + } +} + +export default withDraggable( MyDraggable ); +``` \ No newline at end of file diff --git a/packages/components/src/higher-order/with-draggable/index.js b/packages/components/src/higher-order/with-draggable/index.js new file mode 100644 index 00000000000000..16ab27997d5531 --- /dev/null +++ b/packages/components/src/higher-order/with-draggable/index.js @@ -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( + ( 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 ( + + ); + } + }; + }, + 'withDraggable' +); + +export default withDraggable; diff --git a/packages/components/src/higher-order/with-draggable/style.scss b/packages/components/src/higher-order/with-draggable/style.scss new file mode 100644 index 00000000000000..ad046f4e092a36 --- /dev/null +++ b/packages/components/src/higher-order/with-draggable/style.scss @@ -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; +} diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 182cb6e111c0d3..402e220828286a 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -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'; diff --git a/packages/editor/src/components/block-list/block-draggable.js b/packages/editor/src/components/block-list/block-draggable.js deleted file mode 100644 index 67f77189b36bd2..00000000000000 --- a/packages/editor/src/components/block-list/block-draggable.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { Draggable } from '@wordpress/components'; - -function BlockDraggable( { rootClientId, index, clientId, layout, isDragging, ...props } ) { - const className = classnames( 'editor-block-list__block-draggable', { - 'is-visible': isDragging, - } ); - - const transferData = { - type: 'block', - fromIndex: index, - rootClientId, - clientId, - layout, - }; - - return ( - -
-
- ); -} - -export default BlockDraggable; diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 4d32a8069dc125..91fd06c5611b40 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -44,7 +44,6 @@ import BlockContextualToolbar from './block-contextual-toolbar'; import BlockMultiControls from './multi-controls'; import BlockMobileToolbar from './block-mobile-toolbar'; import BlockInsertionPoint from './insertion-point'; -import BlockDraggable from './block-draggable'; import IgnoreNestedEvents from './ignore-nested-events'; import InserterWithShortcuts from '../inserter-with-shortcuts'; import Inserter from '../inserter'; @@ -394,7 +393,7 @@ export class BlockListBlock extends Component { // We render block movers and block settings to keep them tabbale even if hidden const shouldRenderMovers = ( isSelected || hoverArea === 'left' ) && ! showEmptyBlockSideInserter && ! isMultiSelecting && ! isPartOfMultiSelection && ! isTypingWithinBlock; const shouldRenderBlockSettings = ( isSelected || hoverArea === 'right' ) && ! isMultiSelecting && ! isPartOfMultiSelection; - const shouldShowBreadcrumb = isHovered && ! isEmptyDefaultBlock; + const shouldShowBreadcrumb = ( isSelected || isHovered ) && ! isEmptyDefaultBlock; const shouldShowContextualToolbar = ! showSideInserter && ( ( isSelected && ! isTypingWithinBlock && isValid ) || isFirstMultiSelected ) && ( ! hasFixedToolbar || ! isLargeViewport ); const shouldShowMobileToolbar = shouldAppearSelected; const { error, dragging } = this.state; @@ -481,18 +480,6 @@ export class BlockListBlock extends Component { ] } { ...wrapperProps } > - { ! isPartOfMultiSelection && isMovable && ( - - ) } { shouldShowInsertionPoint && ( ) } { shouldShowContextualToolbar && } diff --git a/packages/editor/src/components/block-list/breadcrumb.js b/packages/editor/src/components/block-list/breadcrumb.js index ca14b1d346c947..cb182c36e5c53e 100644 --- a/packages/editor/src/components/block-list/breadcrumb.js +++ b/packages/editor/src/components/block-list/breadcrumb.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { Component, Fragment } from '@wordpress/element'; -import { Toolbar } from '@wordpress/components'; +import { Dashicon, Toolbar, withDraggable } from '@wordpress/components'; import { withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -28,6 +28,8 @@ export class BlockBreadcrumb extends Component { }; this.onFocus = this.onFocus.bind( this ); this.onBlur = this.onBlur.bind( this ); + this.onDragStart = this.onDragStart.bind( this ); + this.onDragEnd = this.onDragEnd.bind( this ); } onFocus( event ) { @@ -47,12 +49,27 @@ export class BlockBreadcrumb extends Component { } ); } + onDragStart( event ) { + const { draggableData, draggableElementId, initDragging, onDragStart } = this.props; + initDragging( draggableElementId, draggableData )( event ); + onDragStart( event ); + } + + onDragEnd( event ) { + this.props.onDragEnd( event ); + } + render() { - const { clientId, rootClientId } = this.props; + const { isDraggable, clientId, rootClientId } = this.props; return ( -
+
+
{ rootClientId && ( @@ -70,9 +87,9 @@ export default compose( [ withSelect( ( select, ownProps ) => { const { getBlockRootClientId } = select( 'core/editor' ); const { clientId } = ownProps; - return { rootClientId: getBlockRootClientId( clientId ), }; } ), + withDraggable, ] )( BlockBreadcrumb ); diff --git a/packages/editor/src/components/block-list/style.scss b/packages/editor/src/components/block-list/style.scss index 31a991f0c4ef82..63765e5f8123e7 100644 --- a/packages/editor/src/components/block-list/style.scss +++ b/packages/editor/src/components/block-list/style.scss @@ -1,19 +1,4 @@ .editor-block-list__layout .components-draggable__clone { - & > .editor-block-list__block > .editor-block-list__block-draggable { - background: $white; // @todo: ensure this works with themes that invert the color - box-shadow: $shadow-popover; - - @include break-small { - left: -$block-parent-side-ui-clearance - $border-width; - right: -$block-parent-side-ui-clearance - $border-width; - - .editor-block-list__layout & { - left: -$border-width; - right: -$border-width; - } - } - } - // Hide the Block UI when dragging the block // This ensures the page scroll properly (no sticky elements) .editor-block-contextual-toolbar, @@ -24,62 +9,6 @@ } } -.editor-block-list__layout .editor-block-list__block-draggable { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: z-index(".editor-block-list__block-draggable"); - - > .editor-block-list__block-draggable-inner { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - visibility: hidden; - - // Use opacity to work in various editor styles. - background-color: $dark-opacity-light-200; - - .is-dark-theme & { - background-color: $light-opacity-light-200; - } - - @include break-small { - margin: 0 48px; - } - } - - &.is-visible > .editor-block-list__block-draggable-inner { - visibility: visible; - } - - @include break-small { - // use a wider available space for hovering/selecting/dragging on top level blocks - left: -$parent-block-padding - $block-padding; - right: -$parent-block-padding - $block-padding; - - // use smaller space for hovering/selecting/dragging on child blocks - .editor-block-list__layout & { - left: 0; - right: 0; - } - - // Full width blocks don't have the place to expand on the side - .editor-block-list__block[data-align="full"] & { - left: 0; - right: 0; - } - } - - - cursor: move; // Fallback for IE/Edge < 14 - cursor: grab; -} - - // Allow Drag & Drop when clicking on the empty area of the mover and the settings menu .editor-block-list__layout .editor-block-list__block .editor-block-mover, .editor-block-list__layout .editor-block-list__block .editor-block-settings-menu { @@ -95,7 +24,7 @@ .editor-block-list__block { &.is-hidden *, &.is-hidden > * { - visibility: hidden; + opacity: 0.8; } .editor-block-list__block-edit .reusable-block-edit-panel * { @@ -423,12 +352,6 @@ float: left; } - // There is no side UI clearance on full-wide elements, so they are simply not draggable on the sides - > .editor-block-list__block-draggable { - left: 0; - right: 0; - } - // Position hover label on the right > .editor-block-list__breadcrumb { right: -$border-width; @@ -1006,7 +929,14 @@ .editor-block-list__block:hover & { @include fade_in(0.1s); } + + .editor-block-list__breadcrumb-drag-handle { + margin-right: 2px; + } } + + cursor: move;/* Fallback for IE/Edge < 14 */ + cursor: grab; } .editor-block-list__descendant-arrow::before {