UNPKG

@patternfly/react-core

Version:

This library provides a set of common React components for use with the PatternFly reference implementation.

247 lines • 12.5 kB
import { __rest } from "tslib"; import * as React from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; import { DroppableContext } from './DroppableContext'; import { DragDropContext } from './DragDrop'; // Browsers really like being different from each other. function getDefaultBackground() { const div = document.createElement('div'); document.head.appendChild(div); const bg = window.getComputedStyle(div).backgroundColor; document.head.removeChild(div); return bg; } function getInheritedBackgroundColor(el) { const defaultStyle = getDefaultBackground(); const backgroundColor = window.getComputedStyle(el).backgroundColor; if (backgroundColor !== defaultStyle) { return backgroundColor; } else if (!el.parentElement) { return defaultStyle; } return getInheritedBackgroundColor(el.parentElement); } function removeBlankDiv(node) { if (node.getAttribute('blankDiv') === 'true') { // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; if (child.getAttribute('blankDiv') === 'true') { node.removeChild(child); node.setAttribute('blankDiv', 'false'); break; } } } } // Reset per-element state function resetDroppableItem(droppableItem) { removeBlankDiv(droppableItem.node); droppableItem.node.classList.remove(styles.modifiers.dragging); droppableItem.node.classList.remove(styles.modifiers.dragOutside); droppableItem.draggableNodes.forEach((n, i) => { n.style.transform = ''; n.style.transition = ''; droppableItem.draggableNodesRects[i] = n.getBoundingClientRect(); }); } function overlaps(ev, rect) { return (ev.clientX > rect.x && ev.clientX < rect.x + rect.width && ev.clientY > rect.y && ev.clientY < rect.y + rect.height); } export const Draggable = (_a) => { var { className, children, style: styleProp = {}, hasNoWrapper = false } = _a, props = __rest(_a, ["className", "children", "style", "hasNoWrapper"]); /* eslint-disable prefer-const */ let [style, setStyle] = React.useState(styleProp); /* eslint-enable prefer-const */ const [isDragging, setIsDragging] = React.useState(false); const [isValidDrag, setIsValidDrag] = React.useState(true); const { zone, droppableId } = React.useContext(DroppableContext); const { onDrag, onDragMove, onDrop } = React.useContext(DragDropContext); // Some state is better just to leave as vars passed around between various callbacks // You can only drag around one item at a time anyways... let startX = 0; let startY = 0; let index = null; // Index of this draggable let hoveringDroppable; let hoveringIndex = null; let mouseMoveListener; let mouseUpListener; // Makes it so dragging the _bottom_ of the item over the halfway of another moves it let startYOffset = 0; // After item returning to where it started animation completes const onTransitionEnd = (_ev) => { if (isDragging) { setIsDragging(false); setStyle(styleProp); } }; function getSourceAndDest() { const hoveringDroppableId = hoveringDroppable ? hoveringDroppable.getAttribute('data-pf-droppableid') : null; const source = { droppableId, index }; const dest = hoveringDroppableId !== null && hoveringIndex !== null ? { droppableId: hoveringDroppableId, index: hoveringIndex } : undefined; return { source, dest, hoveringDroppableId }; } const onMouseUpWhileDragging = (droppableItems) => { droppableItems.forEach(resetDroppableItem); document.removeEventListener('mousemove', mouseMoveListener); document.removeEventListener('mouseup', mouseUpListener); document.removeEventListener('contextmenu', mouseUpListener); const { source, dest, hoveringDroppableId } = getSourceAndDest(); const consumerReordered = onDrop(source, dest); if (consumerReordered && droppableId === hoveringDroppableId) { setIsDragging(false); setStyle(styleProp); } else if (!consumerReordered) { // Animate item returning to where it started setStyle(Object.assign(Object.assign({}, style), { transition: 'transform 0.5s cubic-bezier(0.2, 1, 0.1, 1) 0s', transform: '', background: styleProp.background, boxShadow: styleProp.boxShadow })); } }; // This is where the magic happens const onMouseMoveWhileDragging = (ev, droppableItems, blankDivRect) => { // Compute each time what droppable node we are hovering over hoveringDroppable = null; droppableItems.forEach(droppableItem => { const { node, rect, isDraggingHost, draggableNodes, draggableNodesRects } = droppableItem; if (overlaps(ev, rect)) { // Add valid dropzone style node.classList.remove(styles.modifiers.dragOutside); hoveringDroppable = node; // Check if we need to add a blank div row if (node.getAttribute('blankDiv') !== 'true' && !isDraggingHost) { const blankDiv = document.createElement('div'); blankDiv.setAttribute('blankDiv', 'true'); // Makes removing easier let blankDivPos = -1; for (let i = 0; i < draggableNodes.length; i++) { const childRect = draggableNodesRects[i]; const isLast = i === draggableNodes.length - 1; const startOverlaps = childRect.y >= startY - startYOffset; if ((startOverlaps || isLast) && blankDivPos === -1) { if (isLast && !startOverlaps) { draggableNodes[i].after(blankDiv); } else { draggableNodes[i].before(blankDiv); } blankDiv.style.height = `${blankDivRect.height}px`; blankDiv.style.width = `${blankDivRect.width}px`; node.setAttribute('blankDiv', 'true'); // Makes removing easier blankDivPos = i; } if (blankDivPos !== -1) { childRect.y += blankDivRect.height; } } // Insert so drag + drop behavior matches single-list case draggableNodes.splice(blankDivPos, 0, blankDiv); draggableNodesRects.splice(blankDivPos, 0, blankDivRect); // Extend hitbox of droppable zone rect.height += blankDivRect.height; } } else { resetDroppableItem(droppableItem); node.classList.add(styles.modifiers.dragging); node.classList.add(styles.modifiers.dragOutside); } }); // Move hovering draggable and style it based on cursor position setStyle(Object.assign(Object.assign({}, style), { transform: `translate(${ev.pageX - startX}px, ${ev.pageY - startY}px)` })); setIsValidDrag(Boolean(hoveringDroppable)); // Iterate through sibling draggable nodes to reposition them and store correct hoveringIndex for onDrop hoveringIndex = null; if (hoveringDroppable) { const { draggableNodes, draggableNodesRects } = droppableItems.find(item => item.node === hoveringDroppable); let lastTranslate = 0; draggableNodes.forEach((n, i) => { n.style.transition = 'transform 0.5s cubic-bezier(0.2, 1, 0.1, 1) 0s'; const rect = draggableNodesRects[i]; const halfway = rect.y + rect.height / 2; let translateY = 0; // Use offset for more interactive translations if (startY < halfway && ev.pageY + (blankDivRect.height - startYOffset) > halfway) { translateY -= blankDivRect.height; } else if (startY >= halfway && ev.pageY - startYOffset <= halfway) { translateY += blankDivRect.height; } // Clever way to find item currently hovering over if ((translateY <= lastTranslate && translateY < 0) || (translateY > lastTranslate && translateY > 0)) { hoveringIndex = i; } n.style.transform = `translate(0, ${translateY}px`; lastTranslate = translateY; }); } const { source, dest } = getSourceAndDest(); onDragMove(source, dest); }; const onDragStart = (ev) => { // Default HTML drag and drop doesn't allow us to change what the thing // being dragged looks like. Because of this we'll use prevent the default // and use `mouseMove` and `mouseUp` instead ev.preventDefault(); if (isDragging) { // still in animation return; } // Cache droppable and draggable nodes and their bounding rects const dragging = ev.target; const rect = dragging.getBoundingClientRect(); const droppableNodes = Array.from(document.querySelectorAll(`[data-pf-droppable="${zone}"]`)); const droppableItems = droppableNodes.reduce((acc, cur) => { cur.classList.add(styles.modifiers.dragging); const draggableNodes = Array.from(cur.querySelectorAll(`[data-pf-draggable-zone="${zone}"]`)); const isDraggingHost = cur.contains(dragging); if (isDraggingHost) { index = draggableNodes.indexOf(dragging); } const droppableItem = { node: cur, rect: cur.getBoundingClientRect(), isDraggingHost, // We don't want styles to apply to the left behind div in onMouseMoveWhileDragging draggableNodes: draggableNodes.map(node => (node === dragging ? node.cloneNode(false) : node)), draggableNodesRects: draggableNodes.map(node => node.getBoundingClientRect()) }; acc.push(droppableItem); return acc; }, []); if (!onDrag({ droppableId, index })) { // Consumer disallowed drag return; } // Set initial style so future style mods take effect style = Object.assign(Object.assign({}, style), { top: rect.y, left: rect.x, width: rect.width, height: rect.height, '--pf-c-draggable--m-dragging--BackgroundColor': getInheritedBackgroundColor(dragging), position: 'fixed', zIndex: 5000 }); setStyle(style); // Store event details startX = ev.pageX; startY = ev.pageY; startYOffset = startY - rect.y; setIsDragging(true); mouseMoveListener = ev => onMouseMoveWhileDragging(ev, droppableItems, rect); mouseUpListener = () => onMouseUpWhileDragging(droppableItems); document.addEventListener('mousemove', mouseMoveListener); document.addEventListener('mouseup', mouseUpListener); // Comment out this line to debug while dragging by right clicking // document.addEventListener('contextmenu', mouseUpListener); }; const childProps = Object.assign({ 'data-pf-draggable-zone': isDragging ? null : zone, draggable: true, className: css(styles.draggable, isDragging && styles.modifiers.dragging, !isValidDrag && styles.modifiers.dragOutside, className), onDragStart, onTransitionEnd, style }, props); return (React.createElement(React.Fragment, null, isDragging && (React.createElement("div", Object.assign({ draggable: true }, props, { style: Object.assign(Object.assign({}, styleProp), { visibility: 'hidden' }) }), children)), hasNoWrapper ? (React.cloneElement(children, childProps)) : (React.createElement("div", Object.assign({}, childProps), children)))); }; Draggable.displayName = 'Draggable'; //# sourceMappingURL=Draggable.js.map