clauderic / react-sortable-hoc

A set of higher-order components to turn any list into an animated, accessible and touch-friendly sortable list✌️
https://clauderic.github.io/react-sortable-hoc/
MIT License
10.81k stars 980 forks source link

Re-order multiple items (multiple selection) #50

Closed stouch closed 7 years ago

stouch commented 8 years ago

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) }} />
irfanlone commented 8 years ago

+1

oyeanuj commented 7 years ago

@stouch Have you used this solution in production? If yes, PR-worthy?

furiousOyster commented 7 years ago

@stouch I think it is more user friendly to clump all the selected items into a contiguous block, then drag this around ? I can't think of a use case where a user would select a disjointed group, then expect to move that group around, maintaining the disjointedness. I'm imaginging at onMoveStart to cause them to all clump together, so that if you immediately released the drag, they would have new positions in a single block. (my apologies... this is much harder to describe than to visualize...)

clauderic commented 7 years ago

@furiousOyster I agree. I'm going to close this issue down as I don't think it has to do with multiple item sorting so much as being able to pass a custom sortable helper renderer. Moving the discussion over to #169

stevenfabre commented 7 years ago

I would really love this! Does anyone have any updates on that? Thanks!

ssilve1989 commented 7 years ago

So it it possible to select and move a contiguous block of elements?

oyeanuj commented 7 years ago

@stevenfabre @ssilve1989 On a related issue, @mixa9269 has it working: https://github.com/clauderic/react-sortable-hoc/pull/138#issuecomment-320627060