Coder Social home page Coder Social logo

clauderic / react-sortable-hoc Goto Github PK

View Code? Open in Web Editor NEW
10.7K 82.0 978.0 21.83 MB

A set of higher-order components to turn any list into an animated, accessible and touch-friendly sortable list✌️

Home Page: https://clauderic.github.io/react-sortable-hoc/

License: MIT License

JavaScript 85.29% HTML 7.82% SCSS 6.90%
react sortable drag-and-drop sorting javascript front-end dragging grid higher-order-component

react-sortable-hoc's Issues

Drag-handle problems cross-browser

A few things I noticed:

Firefox:
The drag handle in your demo page does not show up, but it works.
In some projects (not your demo), as you drag to sort elements, it selects the text on the page (also in IE11)
firefox-ie11-drag-selects-text

Safari: when using an image as a drag-handle (not background-image), set the image attribute draggable='false' or else it will try to drag the image as if dragging a copy to the desktop.
safari-drag-image

Chrome: does not work in the responsive simulator
chrome-touch-simulator-dnd

Request: Nested Sortable

Fantastic library, thank you. An idea for a potential feature:

A page shows multiple sets of Todos:

TodoList Category A:

  • TodoItem A.1
  • TodoItem A.2

TodoList Category B:

  • TodoItem B.1
  • TodoItem B.2

I'd like to be able to:

  1. Move a TodoItem from Category A to B
  2. Move the entire TodoList Category B above TodoList Category A

Update: Looks like item 2 is almost working perfectly already. I am only running into a bug where a dragHandle on a TodoItem which is disabled is causing the parent TodoList to get dragged.

Drag helper in wrong styling context

Since the "drag helper" (the new DOM node added to the end of the <body> which is the actual element being dragged) is not within the same parent element as the source being dragged, styles have a strong potential of getting messed up on the helper (which may rely on parent context). In other words if I have:

<body>
  <div class="user-list">
    <div class="user">Draggable User</div>
    <div class="user">Draggable User</div>
  </div>
</body>

I might depend (in CSS) on a .user-list > .user relationship for styling the <div class="user">. But when the drag starts, we essentially get...

<body>
  <div class="user-list">
    <div class="user" style="visibility-hidden">Draggable User</div>
    <div class="user">Draggable User</div>
  </div>

  <!-- Helper (the thing being dragged)-->
  <div class="user" style=" ... ">Draggable User</div>
</body>

This can be fixed if it is possible to put the drag-helper node as a sibling to the original thing being dragged instead of at the end of the body. Thoughts?

BUG: `margin`s are not taken into account in the re-positioning of the items

How to reproduce

  1. Go to the demo page
  2. Add .Showcase__style__stylizedItem {margin-bottom: 10px;} to the styles.
  3. Drag the first item of the list bellow the second and don't release the mouse button.

What was expected

The second item slides all the way to the top.

What happens

The second item slides up but not all the way to the top (leaving a margin above it).

Reason

The items are translated by dimension with is either width or height, which in turn are offsetWidth and offsetHeight respectively
But offsetWidth and offsetHeight do not include the margins MDN.

Snapshots

image

image

Wrapper component instead of an enhancer function (HOC)

First-off thank you for this great library.
I recently realized that it better to have a wrapper component (like what React-Draggable does) then to have an HOC.
And so I was wandering what is the reasoning behind this choice, and if it's possible to have a non HOC version of the wrappers.

I would be happy to do a PR to add this feature.

Advantages to using a wrapper component

  • No need for withRef option.
  • Can be used to implement HOC.
  • No displayName generation (=> cleaner in the dev-tools panel)

Usage example

const SortableItem = ({value}) => <li>{value}</li>;
const SortableList = ({items}) => (
    <ul>
        {items.map((value, index) => 
            <SortableElement key={`item-${index}`} index={index}>
                <SortableItem value={value} />
            </SortableElement>
        )}
    </ul>
);

class SortableComponent extends Component {
    state = {
        items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6']
    }
    onSortEnd = ({oldIndex, newIndex}) => {
        this.setState({
            items: arrayMove(this.state.items, oldIndex, newIndex)
        });
    };
    render() {
        return (
            <SortableContainer onSortEnd={this.onSortEnd}>
                <SortableList items={this.state.items} />
            </SortableContainer>
        )
    }
}

or

const SortableItem = ({value, ...props}) => (
    <SortableElement {...props}>
        <li>{value}</li>
    </SortableElement>
);
const SortableList = ({items, ...props}) => (
    <SortableContainer {...props}>
        <ul>
            {items.map((value, index) => 
                <SortableItem key={`item-${index}`} index={index} value={value} />
            )}
        </ul>
    </SortableContainer>
);

class SortableComponent extends Component {
    state = {
        items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6']
    }
    onSortEnd = ({oldIndex, newIndex}) => {
        this.setState({
            items: arrayMove(this.state.items, oldIndex, newIndex)
        });
    };
    render() {
        return (
            <SortableList items={this.state.items} onSortEnd={this.onSortEnd} />
        )
    }
}

Error when I testing a sortable component

When i running a test and this component has react-sortable-hoc inmediately throws this error

react-sortable-hoc/dist/commonjs/utils.js:34
  return pre[0].toUpperCase() + pre.substr(1);
TypeError: Cannot read property '0' of undefined

I think this error is beacuse Jsdom, anyone has had this error ? any test works when the component has this lib

Help me :)

Clicking without dragging doesn't update newIndex

It seems like the SortableContainer's newIndex only resets when onSortMove is triggered, which doesn't seem to get fired if you just click a SortableElement without dragging it (which makes sense). Just clicking a SortableElement moves the element to the last newIndex that was set in that container (or 0 if it wasn't) and causes some funky sorting.

The examples (like this one) show this happening too when you click an item besides Item 0.

Width of items with padding/border changes

Because react-sortable-hoc uses offsetWidth (see

this.width = node.offsetWidth;
), which includes borders and paddings (see https://developer.mozilla.org/de/docs/Web/API/HTMLElement/offsetWidth), as the width of the helper, it is too wide. This results in visual "jump" when dragging items.

Possible solutions:

  • always set "box-sizing: border-box" on the helper
  • use clientWidth instead.

Workaround:

  • set "box-sizing: border-box" on your SortableElements manually.

Firefox dirtying

When the component is moved out of the context, In Firefox, it dirty the page.

Se the images
dirtying

dirtyin2

dirtying3

Firefox version: 48.0

Layout changes upon drag

At the moment I have a SortableElement that contains an input textfield and a textarea. When I click to drag the component to a desired location the layout of the SortableElement changes. What extra steps do I need to do to address this issue? Thanks for the help.

before

after

Ability to change handle/card styling onClick

When using the html5 DND api and a handle I can benefit from the :active pseudo class. Since we are creating a copy we don't have an ability to do that. Ideally i'd be able to add some kind of -selected class to my handle or have the :active working on the handle

To give an example of my use case:
My item has a handle and on click + drag the handle changes colour.

Nested sortable elements cause conflict

I have a list of grouped components. I can sort them just fine with the plugin.

But inside my components I have some lists that also use the same plugin. So a sortable inside a sortable. This conflicts with the parent groups. For example, if I have 3 sortable groups, the first 3 list items inside each group conflict with the parents because they share the same index. Once I get to four or five in any list, they sort fine.

I tested setting the index in the nested sortable list to index+10 and it works without conflict, but of course causes other problems because is the wrong index.

Will work on an example showing the problem.

Feature Request: Drag and drop in a Grid

Hi @clauderic! I thought I'll create a standing issue incase anyone else is interested in working on this or has thoughts as well.

The feature request: Be able to drag and drop across axis, i.e., in a grid of any sort. Currently, the library only works one axis at a time.

Based on conversations with @clauderic, I looked into the issue a little bit more and it seems like the changes are only limited to SortableContainer and that too, mostly around calculating offset and managing the translate3d function. I am still trying to familiarize myself with how they really work.

I also imagine we'll need to also add a new prop or modify 'axis' prop to reflect the three choices - x, y or xy.

TypeError: undefined is not an object (evaluating 'array.length')

Unfortunately, I keep getting the following error when trying to sort an array of custom objects:

TypeError: undefined is not an object (evaluating 'array.length')

This error happens after successfully dragging and dropping 1 time. After that, the error appears and I am unable to move any of the sortableItems.

I know the problem lies in the chunk of code:

screenshot 2016-08-25 06 10 11

I have also included the code for my sortable component:

screenshot 2016-08-25 06 10 23

Don't require global babelHelpers in ES6 build

If the user of the library doesn't happen to use the babel helpers transform and the global helpers runtime, there will be a lot of ReferenceError: babelHelpers is not defined that cannot be fixed other than by providing the global helpers.

Clickable links inside items

What is the best way to support links inside sortable items?

Here's a JSFiddle showing the issue: https://jsfiddle.net/lambdazen/6r7r2cva/7/ -- you can try clicking the links once the page has loaded. It doesn't work. However, a right-click followed by "Open in a new tab" works as expected.

My current workaround is to attach onMouseDown to a call to window.open. Is there a better way?

Thanks in advance! Love your library.

Request: prevent dragging on custom handle

I need to implement table with column sorting and in addition to that - table headers should be sortable horizontally

For horizontal headers sorting I've used your great library
Now I need to handle column sorting by clicking arrows (you know, up|down arrows) inside every header row to sort ASC/DESC specific column.

To do this I need somehow cancel event propogation by clicking arrows so that react-sortable-hoc doesn't begin dragging. I've tried

<SortIcon onClick={(e) => {
    e.stopPropagation()
    e.preventDefault()
    onSortColumn(value.alias, users)
    return false
}}/>

but it does not prevent dragging :(

Is it possible?

Multiple Lists.

Hi! Is there a way to define multiple sortable lists in the same page?

Can't drag after removing row

I'm using Redux to manage the state of my app, and after I dispatch an action to remove a specific row, I can only drag from the rows above the ones I deleted. I'm including some gifs to help with the visualization: here and here.

I'm not using the arrayMove function described in the sample code because I was planning on writing out the logic for that myself using Redux, but I fear that may be the issue.

No animation in IE11

On chrome example works ultra smooth and fast, however in IE11 there is NO animations at all. How can i eliminate this ?

Sometimes doesn't work in Chrome

When first loaded in a new Chrome window, dragging works fine, then for some reason it just stops working in every tab on that window. If I open a new window it works again. Both my code and the examples stop working with no indication to a problem in the console.

Request: HOC to prevent drag from starting on given element

The use case would be a non-draggable button nested inside a draggable list item.

Right now, the workarounds are to either attach native click events to the button (requiring a ref and extra lifecycle handlers), or to add a delay and hope the users click the button faster than the delay (but that reduces drag responsiveness). Otherwise, the dragging event takes precedence and blocks clicks to any clickable thing inside the SortableElement.

SortableHandle could also be a workaround, but that comes at the expense of being able to drag the SortableElement by dragging its background.

ReactCSSTransitionGroup when removing/adding items to sortable not working

I tried using the ReactCSSTransitionGroup element with your great sortable and can make the sortable animate on initial mount (although looks jerky for some reason). But was unable to make it able to animate the adding/removing of individual items to a sortable. Similar code does work for normal object lists where you remove/add to them. Do you have any examples or ideas why it isn't working with a Sortable? I forked another fiddle to show what I mean https://jsfiddle.net/r2LpL7be/

And here is an example of a standard array with splice + animation: http://codepen.io/agrewell/pen/ZYdGOJ

Basic styles?

Nice library. Loving it.

It would be nice if you could include in the repo some basic styles to give developers (especially those not good at making things look great) a starting point to then further customize.

Custom events inside sortables

Hi there!
I need to use custom events on elements inside SelectableElement. But Selectable blocks onClick events (I just can bind onMouseDown / onMouseMove and etc, but not onMouseUp or onClick).
Is there any easy way to solve it?

Thanks!

When the sort starts (mouse down) the helper is added but the ghost is not hidden

How to reproduce

  1. Go to the demo page
  2. Add .Showcase__style__stylizedHelper {background: transparent;} to the styles.
  3. Press (without moving or releasing the mouse) on an item of the list.

What happens

The item is still visible (white background), if you move the mouse the item is now hidden (background grey)

What was expected

The item is hidden (background grey)

I can submit a PR if needed.

Jquery Portability

Is there a way to get this on Jquery ? I was trying to read the source code but no idea how react system works so I was hoping that this can get ported to jQuery or native js.

Request: Don't start dragging unless cursor been dragged x pixels.

I ran into an issue where I have items that expand when clicked. When making them sortable they no longer respond to a click, instead they become dragged immediately. I can get around this by setting the "pressDelay" but then you have to wait for the delay before it's draggable which I noticed that people tend not to do.
I'd like a way to not start the sorting unless you have actually dragged the cursor for say 5px.

Request: Ignore right clicks

Right now, right clicking will initiate a drap&drop, even if the intent is actually to open the context menu.

Better Docs

I have no idea how to get the EXACT replica of the basic usage which is shown on the index page

Like no docs for that, I could get the plugin working but I do not know how to show the dragging element preview to follow the cursor like in the example

Adding and Removing items

I'm trying to add and remove items from the lists, using onClick on buttons. Doing Add was easy using the SortableComponent, but I don't know how to use remove because SortableList and SortableItem are functions not classes to add a method(code below and attached file). I'm just starting working, with React ES6, maybe is something that I don't understand.

Thanks.

const SortableList = SortableContainer(({items}) => {
    return (
        <table>
            {items.map((value, index) =>
            <tr>
                <td>
                <SortableItem key={`item-${index}`} index={index} value={value} />
                    </td>
                    <td>
                    <button>Remove{index}</button>
                </td>
             </tr>
            )}
        </table>
    );
});

class SortableComponent extends React.Component{
     constructor(props) {
         super(props);
         this.state = {items: [1, 2, 3, 4]};
         this.onSortEnd = this.onSortEnd.bind(this);
         this.add = this.add.bind(this);
     }


    onSortEnd({oldIndex, newIndex}) {
        this.setState({items: arrayMove(this.state.items, oldIndex, newIndex)});
    }


    add() {
        const items = this.state.items;
        let new_item = this.state.items.length+1;
        items.push(new_item);
        this.setState({items : items})
        console.log(new_item);
    }

    render() {
        return (
            <div>
                <SortableList items={this.state.items} onSortEnd={this.onSortEnd} />
                <button onClick={this.add}>Add Document</button>
            </div>
        );
    }
}

example

Request: eagerOnSortEnd

It would be great to have a prop which is similar to onSortEnd but gets called whilst you are actually dragging (but only when oldIndex and newIndex have different values from when eagerOnSortEnd was last called).

This way, for example, the "Item 1" and "Item 2" labels of existing list elements can change 'live' whilst I'm dragging to "Item 0", "Item 2" and "Item 0", "Item 1" as I drag the original "Item 0" down the list.

Dealing with dynamically added items (fiddle inside)

Hey Clauderic,

Just played around with this, and it's a great little component! Great job :)

One stumble-block I ran into though was when dealing with dynamically added items. Take a peek at this fiddle:

https://jsfiddle.net/t9va06g5/1/

If you click the add more button, then drag an existing item of the list, it's only then that the newly added item is rendered. Do you know if there is any way to re-render the list when new items are added like this?

Thanks again.

Re-order multiple items (multiple selection)

Hi,

It would be very cool if it was possible to move a list of several items (whatever their positions in the list) at a given position of the list (at sort end, items would be ordered depending on their previous relative positions in the list).

I started something but I think you could do much better.

Thank you :)

import React, {Component} from 'react';
import {SortableContainer, SortableElement} from 'react-sortable-hoc';


const SortableItem = SortableElement(
  class SortableItemAnonymous extends Component {
    onMouseDownCallback( event ){
      return this.props.onMouseDownCallback( this.props.index, event )
    }
    render(){
      var id = this.props.uniqueIdToken + "SortableItem" + this.props.index
      var className = this.props.checked ? "helper checked-sortable-item" : ""
      return (
        <li key={"li-sortable-item-"+id}
            data-sortableId={id}
            style={this.props.style}
            onMouseDown={this.onMouseDownCallback.bind(this)}
            className={className}>
          {this.props.value}
        </li>
      )
    }
  }
)
const SortableList = SortableContainer(
  class SortableListAnonymous extends Component {
    render() {
      var self = this
      return (
        <ul>
          {this.props.items.map((value, index) =>
            {
              var style = {}
              style.visibility = value.visibility ? value.visibility : ''
              value.height = typeof(value.height)!='undefined' ? value.height : value.defaultHeight
              style.height = typeof( value.height ) == 'string' ? value.height : value.height+'px'
              var checked = self.props.selection ? self.props.selection.indexOf(index) > -1 : 0
              return (
                <SortableItem key={`sortable-item-${index}`}
                              style={style}
                              checked={checked}
                              uniqueIdToken={self.props.uniqueIdToken}
                              index={index} value={value.value}
                              onMouseDownCallback={self.props.onMouseDownCallback} />
              )
            }
          )}
        </ul>
      )
    }
  }
)

export class SortableComponent extends Component {
  constructor(props){
    super(props)
    this.state = {
      selected: null,
      selection: [],
      moving: false,
      movingstarted: false,
      items: props.items
    }
  }
  componentWillReceiveProps(nextProps){
    this.setState({
      selected: null,
      selection: [],
      moving: false,
      movingstarted: false,
      items: nextProps.items
    })
  }
  onMouseDownCallback = (index, event) => {
    var newSelection = this.state.selection
    var testIndex = newSelection.indexOf(index)
    if( event.ctrlKey || event.metaKey || this.state.selection.length==0 ) {
      if(newSelection && testIndex != -1 ){
        newSelection.splice(testIndex, 1)
      }else {
        newSelection = newSelection.concat([index])
      }
    }else{
      // si on clique sur un item sans faire CTRL, et quil nest pas encore dans la selection,
      // on met a jour la selection courante juste avec cet item
      if( testIndex == -1 ){
        newSelection = [index]
      }
    }
    this.setState({
      selected: index,
      selection: newSelection.sort((a, b)=>{return a-b})
    })
    event.preventDefault()
    return false
  }
  onSortStart = ({node, index, collection}, event) => {
    this.setState({
      movingstarted: true
    })
  };
  onSortMove = (event) => {

    if( !this.state.moving && this.state.movingstarted ) {
      var selection = this.state.selection
      var selected = this.state.selected
      var items = this.state.items


      var indexSelected = selected
      for (var i = selection.length - 1; i >= 0; i--) {
        var j = selection[i]
        if (j != selected) {
          if (j < indexSelected) indexSelected--
          items[j].height = 0
          items[j].visibility = 'hidden'
        }else{
          items[j].height = items[j].defaultHeight * selection.length
        }
      }

      // DOM MANAGEMENT
      if( selection.length > 1 ) {
        let helpers = document.getElementsByClassName('helper')
        let hl = helpers.length - 1
        /* helpers[hl].innerHTML = ''
         for (let i = 0; i < selection.length; i++ ) {
         let selindex = selection[i]
         let value = this.props.uniqueIdToken+"SortableItem"+selindex
         helpers[hl].innerHTML += ''+document.querySelector('[data-sortableId="' + value + '"]').outerHTML+'';
         }*/
        helpers[hl].innerHTML = selection.length + ' ' + this.props.multipleSelectionLabel
      }
      // END DOM MANAGEMENT

      this.setState({
        items: items,
        moving: true
      })
    }

  };
  onSortEnd = ({oldIndex, newIndex}) => {
    if( this.state.moving && this.state.movingstarted ) {
      if (this.state.selection.length > 0) {

        var newOrder = []
        // new order of index (array of values where values are old indexes)
        // it depends if we've "upped" the list (newIndex < oldIndex) or "downed" it
        var toPushInNewOrderLater = []
        for( var idx = 0; idx < this.state.items.length; idx++ ){
          if( this.state.selection.indexOf(idx) == -1 ) {
            if( newIndex>oldIndex ) {
              if (idx <= newIndex) {
                newOrder.push(idx)
              } else if (idx > newIndex) {
                toPushInNewOrderLater.push(idx)
              }
            }else{
              if (idx < newIndex) {
                newOrder.push(idx)
              } else if (idx >= newIndex) {
                toPushInNewOrderLater.push(idx)
              }
            }
          }
        }
        newOrder = newOrder.concat(this.state.selection).concat(toPushInNewOrderLater)


        var newitems = this.state.items
        var newselection = this.state.selection
        var newselected = this.state.selected

        // Pour determiner la nouvelle liste ditems, on commence par supprimer tous les index de la selection
        // Quand on supprime un item dont lindex est avant le newIndex, on decremente le newIndex
        var selectionToPush = []
        for (var i = this.state.selection.length - 1; i >= 0; i--) {
          var index = this.state.selection[i]
          if (index < newIndex && index != this.state.selected) newIndex--
          selectionToPush.unshift(newitems[index])
          newitems.splice(index, 1)
        }
        // a present, on insere au niveau de newIndex, la liste ordonnée de la selection
        // pour chacun on remet la hauteur et la visibilité par defaut
        var k = 0
        for (var i = 0; i < selectionToPush.length; i++) {
          selectionToPush[i].height = selectionToPush[i].defaultHeight
          selectionToPush[i].visibility = 'visible'
          newitems.splice(newIndex + k, 0, selectionToPush[i])
          k++
        }
        // sil y a eu changement de tri, ou qu'on a selectionné plusieurs items
        if (oldIndex != newIndex || (oldIndex == newIndex && this.state.selection.length > 1)) {
          // on clear la selection
          newselection = []
          newselected = null
        }

        // mise a jour du state local
        this.setState({
          items: newitems,
          selected: newselected,
          selection: newselection,
          moving: false,
          movingstarted: false
        });

        this.props.callbackNewOrder( newOrder )
      }
    }
  };
  render() {
    return (
      <SortableList uniqueIdToken={this.props.uniqueIdToken}
                    items={this.state.items}
                    selection={this.state.selection}
                    selected={this.state.selected}
                    helperClass="helper"
                    onMouseDownCallback={this.onMouseDownCallback}
                    onSortEnd={this.onSortEnd}
                    onSortStart={this.onSortStart}
                    onSortMove={this.onSortMove}
                    useDragHandle={false}
                    distance={10} />
    )
  }
}

USAGE :

let items = [
{value:"item 1", defaultHeight:10},
{value:"item 2", defaultHeight:10},
{value:"item 3", defaultHeight:10}
]

<SortableComponent items={items}
                             uniqueIdToken="test"
                             multipleSelectionLabel=" items selected"
                             callbackNewOrder={(oldIndexesWithNewOrder) => { console.log(oldIndexesWithNewOrder) }} />

If VirtualScroll is wrapped inside AutoSize dragging an item doesn't trigger scroll

Awesome job @clauderic 👍

I'm having a huge list that I'm using react-virtualized but if I want the width to be dynamic, for this I must wrap it with AutoSize You should already know this 😊

class VirtualList extends Component {
  render() {
    const { items } = this.props;

    return (
      <AutoSizer disableHeight>
        {({ width }) => (
          <VirtualScroll
            ref="VirtualScroll"
            estimatedRowSize={70}
            rowHeight={70}
            rowRenderer={({ index }) => {
              const { product } = items[index];
              return <SortableItem index={index} product={product} />;
            }}
            rowCount={items.length}
            width={width}
            height={500}
          />
        )}
      </AutoSizer>
    );
  }
}

So when I do, I lose the ability to scroll while dragging & item, any hints how to fix this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.