2

I have a react DND item on a 'board' which triggers a page scroll if you pull it far up/left/right/down from its original position.

The scrolling during hover is working well, but I have an issue where when I drop the item, it only drops relative to the cursor's position on the screen. It does not account for the additional movement from the auto scrolling. (e.g. if you pull it to the right of the screen and scroll all the way to the right of the board, when you drop the item it will only drop a few hundred pixels to the right, as if no scrolling had occurred).

I have tried to cater for this by adding a window scroll event listener and adding the distance scrolled onto the drop position. BUT, the state in the DOM is using the old state from when the item was picked up and not adding any of the scrolling to the final coordinates of the page.

Am I going about this all wrong? I have tried some packages like react-dnd-scrollzone, but it looks like they are no longer supported with newer versions of react.

Can anyone provide some insight on how you can account for window scrolling with react DND drop events?

The code I have below ALMOST works, except the item drop only accepts the x and y adjustments the next time AFTER a drop event has been completed, it cannot take the updated state on item drop (it is working on the old state), so the next time you drop an item it moves to the correct position factoring in the scrolling from your last drop. It is always one item drop behind the current state because x and y adjustment state isn't pulled into the DOM until after a drop event is complete....

Here is my code for an item drop event and for page scrolling and the scroll window event listener.

DND Item Drop and Hover Event. The originating scroll coordinates are set on the first instance of 'hover'

          const [dragStarted, setDragStarted] = useState(false);
          const [origXScroll, setOrigXScroll] = useState(0);
          const [origYScroll, setOrigYScroll] = useState(0);
          const [xAdjustment, setXAdjustment] = useState(0);
          const [yAdjustment, setYAdjustment] = useState(0);
    
     const updatePageState = useCallback((droppedPage) => {
      const updatedPages = pages.map(page => droppedPage._id === page._id ? droppedPage : page);
      changedPagesCallback(updatedPages);
      setTreePages(updatedPages);
        }, [pages]);
      const [{}, drop] = useDrop(() => ({
            accept: ItemTypes.PAGECARD,
            hover(page, monitor) {
                if(!dragStarted) {
                setDragStarted(true)
                setOrigXScroll(xAdjustment)
                setOrigYScroll(yAdjustment)};
                const clientOffset = monitor.getClientOffset();
                const origPosition = monitor.getInitialClientOffset();
                const origX = origPosition.x;
                const origY = origPosition.y;
                const hoverY = clientOffset.y;
                const hoverX = clientOffset.x;
                checkPageScroll(origX, origY, hoverX, hoverY);
            },
            drop(page, monitor) {
                setDragStarted(false);
                const delta = monitor.getDifferenceFromInitialOffset();
//The issue lies in that difference from initial offset only gives mouse position on the screen and doesn't account for any auto scrolling on the page...
                const xAdjuster = xAdjustment < origXScroll ? (xAdjustment - origXScroll) : xAdjustment;
                const yAdjuster = yAdjustment < origYScroll ? (yAdjustment - origYScroll) : yAdjustment;
                let x = Math.round(page.x + (delta.x)*(1/pageZoomFactor) + xAdjuster);
                let y = Math.round(page.y + (delta.y) + yAdjuster);
                page = Object.assign(page, {x: x, y: y})
                saveUpdatedPage({x: x, y:y}, page._id);
                updatePageState(page);
                setOrigXScroll(0);
                setOrigYScroll(0);
                setXAdjustment(0);
                setYAdjustment(0);
                return undefined;
            },
        }), [updatePageState]);

Auto Scroller if you pull item far to any side

function checkPageScroll(origX, origY, hoverX, hoverY){
  const xScroll = (hoverX - origX)/10;
  const yScroll = (hoverY - origY)/10;
  const allowedXScroll = xScroll > 8 || xScroll < -8 ? xScroll : 0;
  const allowedYScroll = yScroll > 8 || yScroll < -8 ? yScroll : 0;
  window.scrollBy(allowedXScroll, allowedYScroll);
}

Window Event Listener to Set the Amount you Have Scrolled. At the start of drag the scroll coordinates are locked as origXScroll and origYScroll, the idea is to add/subtract distance travelled by scrolling left/right up/down on the drop event

useEffect(() => {
  window.addEventListener('scroll', () => {
    const yScroll = window.scrollY;
    const xScroll = window.scrollX;
    setXAdjustment(xScroll);
    setYAdjustment(yScroll);
  })
}, [])
John Shanks
  • 165
  • 9

1 Answers1

1

OK, after a mountain of digging I came across the following forum posts in the react dnd issues and discovered a few more tools to get this done. https://github.com/react-dnd/react-dnd/issues/151

Instead of measuring the amount the page has scrolled and adding that to the final coordinates, a better method is to use getInitialSourceClientOffset() and getSourceClientOffset() (both of which I had no idea existed....) to compare the items start and end location.

To do this you will also need a referenced element to compare your items position (can just be a referenced div).

This method still needs a bit of tweaking to account for page zoom, but it is the best I've seen so far. I'm still working on this, but here's a simplified version of the code for dropping new pages onto the board to see how it could potentially all be put together. I'd recommend using the solutions on the react DND issues link above and reading their posts to get a good idea on how the professionals manage this problem.

import React, { useCallback, useEffect, useState, useRef } from 'react';
import { useDrop } from "react-dnd";
import { ItemTypes } from "../Utils/items";


function PageTree({ pages }) {
  const containerRef = useRef("pageBoard");
  const [dragStarted, setDragStarted] = useState(false);

  //this function to update the state of a page card when a change is made to its position or a widget added
  const updatePageState = useCallback((droppedPage) => {
  const updatedPages = pages.map(page => droppedPage._id === page._id ? droppedPage : page);
  changedPagesCallback(updatedPages);
  setTreePages(updatedPages);
    }, [pages]);

  const [{updateY, updateX}, drop] = useDrop(() => ({
        accept: ItemTypes.PAGECARD,
        hover(page, monitor) {
            if(!dragStarted) {
            setDragStarted(true)
            setOrigXScroll(xAdjustment)
            setOrigYScroll(yAdjustment)
            };
            const clientOffset = monitor.getClientOffset();
            const origPosition = monitor.getInitialClientOffset();
            const origX = origPosition.x;
            const origY = origPosition.y;
            const hoverY = clientOffset.y;
            const hoverX = clientOffset.x;
            checkPageScroll(origX, origY, hoverX, hoverY);
        },
        drop(page, monitor) {
            setDragStarted(false);
            const initialPosition = monitor.getInitialSourceClientOffset();
            const finalPosition = monitor.getSourceClientOffset();
            const container = containerRef.current.getBoundingClientRect();
            const newXY = getCorrectDroppedOffsetValue(initialPosition,finalPosition,container);
            page = Object.assign(page, {x: newXY.x, y: newXY.y})
            saveUpdatedPage({x: newXY.x, y:newXY.y}, page._id);
            updatePageState(page);
            return undefined;
        },
    }), [updatePageState]);
    
    useEffect(() => {
      OriginalScrollFunc.current = scrollTo
    }, [pages]);

    function checkPageScroll(origX, origY, hoverX, hoverY){
      const xScroll = (hoverX - origX)/10;
      const yScroll = (hoverY - origY)/10;
      const allowedXScroll = xScroll > 8 || xScroll < -8 ? xScroll : 0;
      const allowedYScroll = yScroll > 8 || yScroll < -8 ? yScroll : 0;
      window.scrollBy(allowedXScroll, allowedYScroll);
    }

    function getCorrectDroppedOffsetValue (initialPosition, finalPosition, dropTargetPosition) {
      // get the container (view port) position by react ref...
      //const dropTargetPosition = ref.current.getBoundingClientRect();
      const { y: finalY, x: finalX } = finalPosition;
      const { y: initialY, x: initialX } = initialPosition;
      
      // calculate the correct position removing the viewport position.
     // finalY > initialY, I'm dragging down, otherwise, dragging up
      const newYposition =
        finalY > initialY
          ? initialY + (finalY - initialY) - dropTargetPosition.top
          : initialY - (initialY - finalY) - dropTargetPosition.top;
  
      const newXposition =
        finalX > initialX
          ? initialX + (finalX - initialX) - dropTargetPosition.left
          : initialX - (initialX - finalX) - dropTargetPosition.left;
      return {
        x: newXposition,
        y: newYposition,
      };
    };
  }

  return (
        <div ref={containerRef} style={{postion:'relative'}}>
          <div ref={drop} style={styles}>
            {items.map = > things you drop around}
          </div>
        </div>
      )
}

export default PageTree; 
John Shanks
  • 165
  • 9