UNPKG

@wordpress/components

Version:
235 lines (218 loc) 8.02 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Draggable = Draggable; exports.default = void 0; var _compose = require("@wordpress/compose"); var _element = require("@wordpress/element"); var _jsxRuntime = require("react/jsx-runtime"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ const dragImageClass = 'components-draggable__invisible-drag-image'; const cloneWrapperClass = 'components-draggable__clone'; const clonePadding = 0; const bodyClass = 'is-dragging-components-draggable'; /** * `Draggable` is a Component that provides a way to set up a cross-browser * (including IE) customizable drag image and the transfer data for the drag * event. It decouples the drag handle and the element to drag: use it by * wrapping the component that will become the drag handle and providing the DOM * ID of the element to drag. * * Note that the drag handle needs to declare the `draggable="true"` property * and bind the `Draggable`s `onDraggableStart` and `onDraggableEnd` event * handlers to its own `onDragStart` and `onDragEnd` respectively. `Draggable` * 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. * * ```jsx * import { Draggable, Panel, PanelBody } from '@wordpress/components'; * import { Icon, more } from '@wordpress/icons'; * * const MyDraggable = () => ( * <div id="draggable-panel"> * <Panel header="Draggable panel"> * <PanelBody> * <Draggable elementId="draggable-panel" transferData={ {} }> * { ( { onDraggableStart, onDraggableEnd } ) => ( * <div * className="example-drag-handle" * draggable * onDragStart={ onDraggableStart } * onDragEnd={ onDraggableEnd } * > * <Icon icon={ more } /> * </div> * ) } * </Draggable> * </PanelBody> * </Panel> * </div> * ); * ``` */ function Draggable({ children, onDragStart, onDragOver, onDragEnd, appendToOwnerDocument = false, cloneClassname, elementId, transferData, __experimentalTransferDataType: transferDataType = 'text', __experimentalDragComponent: dragComponent }) { const dragComponentRef = (0, _element.useRef)(null); const cleanupRef = (0, _element.useRef)(() => {}); /** * Removes the element clone, resets cursor, and removes drag listener. * * @param event The non-custom DragEvent. */ function end(event) { event.preventDefault(); cleanupRef.current(); if (onDragEnd) { onDragEnd(event); } } /** * This method does a couple of things: * * - 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 event The non-custom DragEvent. */ function start(event) { const { ownerDocument } = event.target; event.dataTransfer.setData(transferDataType, JSON.stringify(transferData)); const cloneWrapper = ownerDocument.createElement('div'); // Reset position to 0,0. Natural stacking order will position this lower, even with a transform otherwise. cloneWrapper.style.top = '0'; cloneWrapper.style.left = '0'; const dragImage = ownerDocument.createElement('div'); // 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) { dragImage.classList.add(dragImageClass); ownerDocument.body.appendChild(dragImage); event.dataTransfer.setDragImage(dragImage, 0, 0); } cloneWrapper.classList.add(cloneWrapperClass); if (cloneClassname) { cloneWrapper.classList.add(cloneClassname); } let x = 0; let y = 0; // If a dragComponent is defined, the following logic will clone the // HTML node and inject it into the cloneWrapper. if (dragComponentRef.current) { // Position dragComponent at the same position as the cursor. x = event.clientX; y = event.clientY; cloneWrapper.style.transform = `translate( ${x}px, ${y}px )`; const clonedDragComponent = ownerDocument.createElement('div'); clonedDragComponent.innerHTML = dragComponentRef.current.innerHTML; cloneWrapper.appendChild(clonedDragComponent); // Inject the cloneWrapper into the DOM. ownerDocument.body.appendChild(cloneWrapper); } else { const element = ownerDocument.getElementById(elementId); // Prepare element clone and append to element wrapper. const elementRect = element.getBoundingClientRect(); const elementWrapper = element.parentNode; const elementTopOffset = elementRect.top; const elementLeftOffset = elementRect.left; cloneWrapper.style.width = `${elementRect.width + clonePadding * 2}px`; const clone = element.cloneNode(true); clone.id = `clone-${elementId}`; // Position clone right over the original element (20px padding). x = elementLeftOffset - clonePadding; y = elementTopOffset - clonePadding; cloneWrapper.style.transform = `translate( ${x}px, ${y}px )`; // Hack: Remove iFrames as it's causing the embeds drag clone to freeze. Array.from(clone.querySelectorAll('iframe')).forEach(child => child.parentNode?.removeChild(child)); cloneWrapper.appendChild(clone); // Inject the cloneWrapper into the DOM. if (appendToOwnerDocument) { ownerDocument.body.appendChild(cloneWrapper); } else { elementWrapper?.appendChild(cloneWrapper); } } // Mark the current cursor coordinates. let cursorLeft = event.clientX; let cursorTop = event.clientY; function over(e) { // Skip doing any work if mouse has not moved. if (cursorLeft === e.clientX && cursorTop === e.clientY) { return; } const nextX = x + e.clientX - cursorLeft; const nextY = y + e.clientY - cursorTop; cloneWrapper.style.transform = `translate( ${nextX}px, ${nextY}px )`; cursorLeft = e.clientX; cursorTop = e.clientY; x = nextX; y = nextY; if (onDragOver) { onDragOver(e); } } // Aim for 60fps (16 ms per frame) for now. We can potentially use requestAnimationFrame (raf) instead, // note that browsers may throttle raf below 60fps in certain conditions. // @ts-ignore const throttledDragOver = (0, _compose.throttle)(over, 16); ownerDocument.addEventListener('dragover', throttledDragOver); // Update cursor to 'grabbing', document wide. ownerDocument.body.classList.add(bodyClass); if (onDragStart) { onDragStart(event); } cleanupRef.current = () => { // Remove drag clone. if (cloneWrapper && cloneWrapper.parentNode) { cloneWrapper.parentNode.removeChild(cloneWrapper); } if (dragImage && dragImage.parentNode) { dragImage.parentNode.removeChild(dragImage); } // Reset cursor. ownerDocument.body.classList.remove(bodyClass); ownerDocument.removeEventListener('dragover', throttledDragOver); }; } (0, _element.useEffect)(() => () => { cleanupRef.current(); }, []); return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [children({ onDraggableStart: start, onDraggableEnd: end }), dragComponent && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: "components-draggable-drag-component-root", style: { display: 'none' }, ref: dragComponentRef, children: dragComponent })] }); } var _default = exports.default = Draggable; //# sourceMappingURL=index.js.map