Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for dragging items between Lists #138

Closed
wants to merge 38 commits into from

Conversation

jdbence
Copy link

@jdbence jdbence commented Feb 5, 2017

drag

  • Changed stories port to 8082 so it can be used online
  • Add stories for horizontal, vertical and grid lists
  • Add labels to storybook list items so they can be differentiated

@jdbence jdbence mentioned this pull request Feb 5, 2017
@oyeanuj
Copy link

oyeanuj commented Feb 5, 2017

@jdbence This looks great! A couple of questions -

  1. Do we need to distinguish between collection property and SortableGroup since I think the difference would be confusing?
  2. I am trying to add the ability to be able to drop without sorting (like a drop-target). What changes can you anticipate that I would need to make in SortableGroup (assuming its merged)?

@jdbence
Copy link
Author

jdbence commented Feb 5, 2017

@oyeanuj

  1. You should not have to distinguish.
  2. A prop will need to be passed and closestNodeIndex would need to return node.length. Everything else should be handled by the original classes.

@clauderic clauderic mentioned this pull request Feb 9, 2017
@alexshelkov
Copy link

It is really interesting feature will it be merged soon?

@clauderic
Copy link
Owner

I've had the chance to test this PR out, and it does work fairly well 👍

Still need to carefully review the implementation though, I glanced quickly at it and had a few questions/suggestions for improvements. Stay tuned.

@Robinfr Robinfr mentioned this pull request Feb 13, 2017
@jakedowns
Copy link

jakedowns commented Feb 14, 2017

Thank you for posting this PR (and for the original HoC!), saved me a bunch of time!

here's a small tweak I had to make to this PR code for it to work better for my project.

i had to change the list.rect.center distance check, to use a list.rect closest edge formula

i think this is due to the fact that my lists had wildly different dimensions, and they're stacked vertically, rather than horizontally

https://gist.github.com/jakedowns/ed421ee3ee57af869fabb594531e6182

i'll try to post a public fork with all my mods if i get a chance to refactor and clean it up.

@oyeanuj
Copy link

oyeanuj commented Feb 14, 2017

@jakedowns Do you think it could be added to this PR with a prop like listArrangement: ('horizontal' or ('vertical')?

@jdbence
Copy link
Author

jdbence commented Feb 14, 2017

@jakedowns Correct, different sized containers will need to check distance with closest edge instead of center. I will update this PR to use similar formula.

@oyeanuj We won't need to send a prop since detection will work in both scenarios.

@jakedowns
Copy link

jakedowns commented Feb 14, 2017

@jdbence note: I had to update my gist, the overlap check was causing issues. nearest-edge check alone seems to suffice: https://gist.github.com/jakedowns/ed421ee3ee57af869fabb594531e6182/revisions

it has the nice added effect of making the list hopping happen based on cursor position, rather than the helper element's boundingClientRect. although, maybe that's a good case for something that could be a toggleable option via a prop.

@jdbence
Copy link
Author

jdbence commented Feb 14, 2017

@jakedowns Yup, I'm only doing the closest edge check since overlap would return the first one found based on order not distance.

@jdbence
Copy link
Author

jdbence commented Feb 15, 2017

@jakedowns Can you verify the fix works with your use case?

@kalimantos
Copy link
Contributor

kalimantos commented Feb 19, 2017

@jdbence great job, only a question: could SortableGroup be implemented using higher-order components like others?

@jdbence
Copy link
Author

jdbence commented Feb 21, 2017

@kalimantos SortableGroup was not created with a HOC so the presentation can remain flexible. Mainly, lists can have the same or separate parents. Always open to suggestions if you have an example!

@kalimantos
Copy link
Contributor

@jdbence yes you're right! i just made a test and it works great, but when i add the prop "useDragHandle" it brokes up after the handleMove is called. The error given to console is
Uncaught TypeError: Cannot read property 'dispatchEvent' of undefined and it happens because in that moment list.helper is not defined (SortableGroup/index.js:106)
How can i solve this?
(I also try using the 'useWindowAsScrollContainer' prop and sometimes has strange effects)

@jdbence
Copy link
Author

jdbence commented Feb 22, 2017

@kalimantos Let me know if the latest changes fix your issue.

@kalimantos
Copy link
Contributor

@jdbence thank you for the fast support, sadly it doesn't fix my problem, but i understand what you're doing. I think the problem is that closest look from the element to their parents. My handler is placed inside the item. So i've implemented a reversed closest so it should search for children

// utils.js
export function closestDescendant(el, fn) {
  // treat always as array also when is a single element
  el = [].concat(el);
  if(el.length === 0) return false;
  // find if at this level some element satisfy our 'fn'
  let found = el.find(fn);  
  if(found) return found;
  // search in all children
  let childList = [].concat.apply([], el.map(el => Array.from(el.childNodes)));
  return closestDescendant(childList, fn);
}

then use it in "startDragging" method instead of "closest"

// SortableGroup/index.js:99
let handle = closestDescendant(target.firstChild, (el) => el.sortableHandle);

and now works

@jdbence
Copy link
Author

jdbence commented Feb 23, 2017

@kalimantos that's correct, searching descendants will work. Hopefully someone makes a PR so the SortableElement will have a ref to the handle.

@kalimantos
Copy link
Contributor

kalimantos commented Feb 23, 2017

@jdbence awesome, thanks a lot

@alexcastillo
Copy link

Great work, @jdbence!

@clauderic Any idea when this PR will be merged? We can't wait to use this feature.

@harklee
Copy link

harklee commented Mar 3, 2017

Thanks for the great work, just tried out and it works flawlessly on a desktop. However, it does not seem to drop the item onto a different list in the "grid mode" on mobile. Can you confirm this? Again, thanks a lot for the great work, this is exactly what I've been looking for.

@fairps
Copy link

fairps commented Jan 28, 2019

@nicubarbaros as far as I am aware the only way that react-beautiful-dnd supports this is by having multiple lists that sit side by side as columns... which does not solve my particular problem since my items can sometimes span multiple columns at once.

@natew
Copy link

natew commented Feb 4, 2019

react-beautiful-dnd doesn't support virtualization, and I had a lot more issues using it out of the box in terms of getting the styles to work.

It would be really nice to see this continue. @sbrichardson how is your branch working? @clauderic would you be open to a PR if it was redone and some people helped test and get it into shape?

@snackycracky
Copy link

since react-beautiful-dnd is not supporting wrapped lists I will use the closed pr here :/ atlassian/react-beautiful-dnd#316

@geminiyellow
Copy link

oh, no, pr and pr and pr, hope react-sortable-hoc can do all.

@mismith
Copy link

mismith commented Jul 4, 2019

Too bad this still isn't supported, right? :(

@denyo
Copy link

denyo commented Jul 8, 2019

I made it work today with two lists and without modifying the library by forwarding events from the source list to the target list. I will post some snippets tomorrow.

@denyo
Copy link

denyo commented Jul 9, 2019

That's how it looks.

The component that controls both lists keeps track of their refs and provides the source list the ref of the target list.
Based on the conditions isDropZoneActive and isHoveringDropZoneTargetList the events of the source list are redirected.

<div
    onMouseEnter={() => this.setState({ isHoveringDropZoneTargetList: true })}
    onMouseLeave={() => this.setState({ isHoveringDropZoneTargetList: false })}
>
    <SkillList
        collection={1}
        skills={targetSkills}
        refList={this.refList1}
    />
>

<SkillList
    collection={2}
    skills={sourceSkills}
    dropSkill={this.dropSkill}
    onSortStartCallback={() => this.setState({ isDropZoneActive: true })}
    onSortEndCallback={() => this.setState({ isDropZoneActive: false })}
    // this is called once the dropzone is hit to trigger the drop and then forward the events from there on
    onSortMoveCallback={
        this.state.isHoveringDropZoneTargetList && this.state.isDropZoneActive
            ? () => this.setState({ isDropZoneActive: false })
            : undefined
    }
    mapEventsToOtherList={
        this.state.isHoveringDropZoneTargetList && !this.state.isDropZoneActive
    }
    refList={this.refList2}
    refOfOtherList={this.refList1}
/>

The crucial part of SkillList itself looks like:

<SortableList
    skills={skills}
    useDragHandle // important
    onSortStart={(sort: SortStart) => {
        setActiveSkill(skills[sort.index]); // local state
        if (onSortStartCallback) {
            onSortStartCallback(); // make drop zones visible
        }
    }}
    onSortEnd={(sort: SortEnd) => {
        if (updateSkillOrder) {
            updateSkillOrder(sort); // actual sorting
        }
        if (onSortEndCallback) {
            onSortEndCallback(); // disable drop zones
        }
        if (!mapEventsToOtherList && dropSkill) {
            dropSkill(activeSkill); // add skill to input component
        }
        setActiveSkill(undefined);
    }}
    onSortMove={(e) => {
        if (onSortMoveCallback && activeSkill) {
            dropSkill(activeSkill); // this removes the skill from the bottom list and adds it to the top list
            setActiveSkill(undefined);
            onSortMoveCallback(); // we hit the drop zone, time to make the overlay disappear

            // replace helper that is being dragged (optional)
            ReactDOM.render(
                <MuiThemeProvider theme={theme}>
                    <SkillItem skill={{ ...activeSkill, disabled: false }} {...props} />
                </MuiThemeProvider>,
                refList.current.helper
            );

            const skillNodes = refOfOtherList.current.container.children;
            const lastNodeIndex = skillNodes.length - 2;
            const lastNode = skillNodes[lastNodeIndex];
            refOfOtherList.current.manager.active = { collection: 1, index: lastNodeIndex };

            // set initial values on other list based on our last move event
            refOfOtherList.current.handlePress(e);

            // manipulate initial offset so that all following events are relative to our initial drop
            refOfOtherList.current.initialOffset = {
                x:
                    e['pageX'] -
                    e['offsetX'] +
                    lastNode.offsetLeft +
                    refList.current.initialOffset.x -
                    refList.current.helper.offsetLeft,
                // if you click the skill in the verticle middle, things should look quite nice
                // on the top or bottom edge, its a bit sketchy
                y: refOfOtherList.current.initialOffset.y - 15,
            };
        }

        if (mapEventsToOtherList && refOfOtherList) {
            refOfOtherList.current.handleMove(e); // forwarding drag events to top list happens here
            refOfOtherList.current.helper.style.opacity = 0; // hide the helper of the top list since we still use the one from the bottom
        }
    }}
    ref={refList}
    {...props}
/>

@geminiyellow
Copy link

wooo! @denyo thanks for your share, you are holy strong.

@slrubinstein
Copy link

slrubinstein commented Sep 10, 2019

I had a similar requirement. In my use case the top list was sortable when dragging from the top or bottom list. The bottom list was not sortable. I solved it by swapping the collection prop during updateBeforeSortStart, which "freezes" the bottom list. Posting a sandbox link in case it is useful to anyone else: https://codesandbox.io/embed/react-sortable-hoc-2-lists-5bmlq

@bfeeley
Copy link

bfeeley commented Nov 8, 2019

What ever happened to this?

@wlemahieu
Copy link

What ever happened to this?

@bfeeley It seems like it was closed due to there being a couple of quality work-arounds provided by @denyo and @slrubinstein above.

@gregoryforel
Copy link

updateBeforeSortStart

That's great @slrubinstein ! Do you have the same example with the ability to drag n drop between both lists?

@slrubinstein
Copy link

@gregoryforel Hi, I don't, but I think if the two lists are part of the same collection you should be able to drag between them. Sorry I can't build it out right now.

@gregoryforel
Copy link

gregoryforel commented Feb 4, 2020 via email

@lochstar
Copy link

Just in case anyone else is trying to find a solution, react-sortablejs works with dragging between multiple lists.

@gregoryforel
Copy link

thanks @lochstar ! Are there things that react-sortablejs can't do that react-sortable-hoc can?

@ftruzzi
Copy link

ftruzzi commented May 17, 2020

Hi! Would anyone here be able to provide a sample codepen using @denyo's approach? I tried implementing it a few weeks ago and failed. I kept using react-beautiful-dnd but I find myself wanting to switch again considering it also doesn't support centered lists.

I'm also wondering if this approach works with touches (on mobile) and if there's a way of "unlocking" the target list when the element starts entering it (instead of the mouse pointer as I think it is in the example gif).

Thanks!

@Darcrandex
Copy link

so what now, is there some demo?

@tangdw
Copy link

tangdw commented Sep 29, 2020

+1

3 similar comments
@iamhuynq
Copy link

iamhuynq commented Oct 6, 2020

+1

@barrychapman
Copy link

+1

@ronanmorris
Copy link

+1

@chaomao
Copy link

chaomao commented Nov 30, 2020

why it is closed? I don't know react-sortable-hoc support it or not...

@rashmimh
Copy link

Do we have demo for this one? Thanks in advance.

@rashmimh
Copy link

That's how it looks.

The component that controls both lists keeps track of their refs and provides the source list the ref of the target list.
Based on the conditions isDropZoneActive and isHoveringDropZoneTargetList the events of the source list are redirected.

<div
    onMouseEnter={() => this.setState({ isHoveringDropZoneTargetList: true })}
    onMouseLeave={() => this.setState({ isHoveringDropZoneTargetList: false })}
>
    <SkillList
        collection={1}
        skills={targetSkills}
        refList={this.refList1}
    />
>

<SkillList
    collection={2}
    skills={sourceSkills}
    dropSkill={this.dropSkill}
    onSortStartCallback={() => this.setState({ isDropZoneActive: true })}
    onSortEndCallback={() => this.setState({ isDropZoneActive: false })}
    // this is called once the dropzone is hit to trigger the drop and then forward the events from there on
    onSortMoveCallback={
        this.state.isHoveringDropZoneTargetList && this.state.isDropZoneActive
            ? () => this.setState({ isDropZoneActive: false })
            : undefined
    }
    mapEventsToOtherList={
        this.state.isHoveringDropZoneTargetList && !this.state.isDropZoneActive
    }
    refList={this.refList2}
    refOfOtherList={this.refList1}
/>

The crucial part of SkillList itself looks like:

<SortableList
    skills={skills}
    useDragHandle // important
    onSortStart={(sort: SortStart) => {
        setActiveSkill(skills[sort.index]); // local state
        if (onSortStartCallback) {
            onSortStartCallback(); // make drop zones visible
        }
    }}
    onSortEnd={(sort: SortEnd) => {
        if (updateSkillOrder) {
            updateSkillOrder(sort); // actual sorting
        }
        if (onSortEndCallback) {
            onSortEndCallback(); // disable drop zones
        }
        if (!mapEventsToOtherList && dropSkill) {
            dropSkill(activeSkill); // add skill to input component
        }
        setActiveSkill(undefined);
    }}
    onSortMove={(e) => {
        if (onSortMoveCallback && activeSkill) {
            dropSkill(activeSkill); // this removes the skill from the bottom list and adds it to the top list
            setActiveSkill(undefined);
            onSortMoveCallback(); // we hit the drop zone, time to make the overlay disappear

            // replace helper that is being dragged (optional)
            ReactDOM.render(
                <MuiThemeProvider theme={theme}>
                    <SkillItem skill={{ ...activeSkill, disabled: false }} {...props} />
                </MuiThemeProvider>,
                refList.current.helper
            );

            const skillNodes = refOfOtherList.current.container.children;
            const lastNodeIndex = skillNodes.length - 2;
            const lastNode = skillNodes[lastNodeIndex];
            refOfOtherList.current.manager.active = { collection: 1, index: lastNodeIndex };

            // set initial values on other list based on our last move event
            refOfOtherList.current.handlePress(e);

            // manipulate initial offset so that all following events are relative to our initial drop
            refOfOtherList.current.initialOffset = {
                x:
                    e['pageX'] -
                    e['offsetX'] +
                    lastNode.offsetLeft +
                    refList.current.initialOffset.x -
                    refList.current.helper.offsetLeft,
                // if you click the skill in the verticle middle, things should look quite nice
                // on the top or bottom edge, its a bit sketchy
                y: refOfOtherList.current.initialOffset.y - 15,
            };
        }

        if (mapEventsToOtherList && refOfOtherList) {
            refOfOtherList.current.handleMove(e); // forwarding drag events to top list happens here
            refOfOtherList.current.helper.style.opacity = 0; // hide the helper of the top list since we still use the one from the bottom
        }
    }}
    ref={refList}
    {...props}
/>

Implementation looks great. Do we have sandbox link for this? I am asking this because I am looking for solution and not sure when this PR will be merged. I wanted to understand this as I have requirement to drag items from one list to another, two lists will be vertically side by side unlike yours. Thanks in advance.

@Gordon-Feng
Copy link

Based on the ideas provided by @denyo , I implemented this feature successfully! , here is the sandbox link: https://codesandbox.io/s/clever-neumann-gvqtv, hope it helps. CC: @rashmimh
drag

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.