'How to scroll while selecting items with mouse in vue

Hi i'm facing the very challenging & interesting problem of scroll during selection of items with mouse drag in both direction i,e up and down

here is a screen shot

enter image description here

Here is my code : https://codesandbox.io/s/select-ivwq8j?file=/src/overridden/Drag-select.vue

Drag-select.vue is the file where drag selection logic is written.

which fires change when files selection gets changed.

I receive those change event here <drag-select-container @change="dragSelect($event)">

Edit 1: after IVO GELO comment

I have added inside drag() function

   try{
      let containerEl = document.querySelector('#wrapping_container');
      let container = containerEl.getBoundingClientRect();
      if(box.top > (container.top )){
          containerEl.scrollTop = box.top - 50;
          return true;
      }
    }catch(e){
      console.log(e);
    } 

Edit code here: https://codesandbox.io/s/select-ivwq8j?file=/src/overridden/Drag-select.vue

It is very interesting and challenging problem so

Please help me thanks in advance!!



Solution 1:[1]

I found a solution for your question. I rewrite your code in a completely different way. Here is a demo that you can test it. Actually it contains two main component. Parent component that is called "HelloCompo" and its code comes here:

HelloCompo:

<template>
  <!-- This is a component that uses "MyDrag" component. -->
  <div id="wrapping_container" class="hello">
    <!-- Here we insert "MyDrag" component that emits custom "change" event when the selection of element is changed according to user drag. -->
    <my-drag @change="dragSelect">
      <div
          class="item"
          :class="{ selected: ( index >= minMax[1] && index <= minMax[0] ) }"
          :key="item"
          v-for="(item, index) in [1, 2, 3, 4,5,6,7,8,9,10,11,12,13,14,15,16]"
      >
        {{ item }}
      </div>
    </my-drag>
  </div>
</template>

<script>
import MyDrag from "./MyDrag";
export default {
  name: "HelloCompo",
  components: {MyDrag},
  data() {
    return {
      selectedItems: [],
    };
  },

  computed: {
    minMax: function () {
      /* This computed property uses data returned by "MyDrag" component to define the maximum and minimum range to accept "selected" class. */
      let max = -1;
      let min = -1;
      if (this.selectedItems.length > 0) {
        max = Math.max(...this.selectedItems);
        min = Math.min(...this.selectedItems);
      }
      return [max-1, min-1]
    }
  },

  methods: {
    dragSelect: function (selectedList) {
      // console.log(selectedList);
      /* this Method is used to set "selectedItems" data after each change in selected drag. */
      this.selectedItems = selectedList;
    }
  },
}
</script>

<style scoped>

.item {
  display: block;
  width: 230px;
  height: 130px;
  background: orange;
  margin-top: 9px;
  line-height: 23px;
  color: #fff;
}
.selected {
  background: red !important;
}
#wrapping_container{
  background:#e7e7e7;
}

</style>

And child component that is called "MyDrag":

MyDrag:

<template>
  <section id="parentAll">
    <!-- I used "@mousedown" and ... for calling methods instead of using all functions in mounted hook. -->
    <div class="minHr" ref="container" @mousedown="startDrag" @mouseup="endDrag" @mousemove="whileDrag">
      <slot></slot>
    </div>

    <!-- This canvas is shown only when the user is dragging on the page. -->
    <canvas ref="myCanvas" v-if="showCanvas" @mouseup="endDrag" @mousemove="whileDrag"></canvas>

  </section>
</template>

<script>
export default {
  name: "MyDrag",
  data() {
    return {
      dragStatus: false, // used for detecting mouse drag
      childrenArr: [], // used to store the information of children of 'ref="container"' that comes from "slot"
      startEvent: null, // used to detect mouse position on mousedown
      endEvent: null, // used to detect mouse position on mouseup
      direction: "topBottom", // used to detect the direction of dragging
      selectedArr: [], // used to store the selected "divs" after dragging
      heightContainer: null, // used to detect the height of 'ref="container"' dynamically
      widthContainer: null, // used to detect the width of 'ref="container"' dynamically
      /* These data used to draw rectangle on canvas while the user is dragging */
      rect: {
        startX: null,
        startY: null,
        w: null,
        h: null
      },
      startDragData: {
        x: null,
        y: null
      },
      whileDragData: {
        x: null,
        y: null,
        CLY: null
      },
      showCanvas: false // used to show or hide <canvas></canvas>
    }
  },
  methods: {
    childrenInfo: function () {
      /* This method is called on "mounted()" hook to gather information about children of 'ref="container"' that comes from <slot></slot> */
      const { container } = this.$refs;
      const stylesDiv = window.getComputedStyle(container, null);
      this.widthContainer = parseFloat( stylesDiv.getPropertyValue("width") );
      this.heightContainer = parseFloat( stylesDiv.getPropertyValue("height") );
      let children = container.childNodes;

      children.forEach((item, index) => {

        let childObj = {
          offsetTop: item.offsetParent.offsetTop + item.offsetTop,
          offsetHeight: item.offsetHeight
        }

        this.childrenArr.push(childObj);
      })
    },

    startDrag: function (event) {
      /* This method is called at mousedown and detect the click or right click. after that it sets some data like "showCanvas". */
        if(event.button === 0) {
          this.dragStatus = true;
          this.startEvent = event.pageY;
          this.startDragData.x = event.pageX;
          this.startDragData.y = event.pageY;
          this.showCanvas = false;
        }
    },

    whileDrag: async function (event) {
      /* This method is called when the user is dragging. Because I want to be confident about showing <canvas> before doing other parts of code, I used "async" function for this method. */
      if (this.dragStatus) {
        await this.showMethod();
        console.log("dragging");
        this.whileDragData.x = event.pageX;
        this.whileDragData.y = event.pageY;
        this.whileDragData.CLY = event.clientY
        await this.canvasMethod();
      } else {
        this.showCanvas = false;
      }
    },

    endDrag: function (event) {
      /* This method is called at mouseup. After that it calls other methods to calculate the "divs" that were selected by user. */
      if(event.button === 0) {
        console.log("end drag");
        this.dragStatus = false;
        this.showCanvas = false;
        this.endEvent = event.pageY;
        this.calculateDirection();
        this.calculateSelected();
      }
    },

    showMethod: function () {
      /* This method is used to set "showCanvas" data at proper time. */
      this.showCanvas = true;
    },

    calculateDirection: function () {
      /* This method is used to detect the direction of dragging. */
      if (this.startEvent <= this.endEvent) {
        this.direction = "topBottom";
      } else {
        this.direction = "bottomTop";
      }
    },

    calculateSelected: function () {
      /* This method is responsible to find out which "divs" were selected while the user was dragging. After that it emits "this.selectedArr" data to the parent component. */
      this.selectedArr = [];
      let endIndex = null;
      let startIndex = null;

      this.childrenArr.forEach( (item, index) => {

        if ( (item.offsetTop < this.endEvent) && ( (item.offsetTop + item.offsetHeight) > this.endEvent) ) {
          endIndex = index;
          console.log(endIndex);
        }

        if ( (item.offsetTop < this.startEvent) && ( (item.offsetTop + item.offsetHeight) > this.startEvent) ) {
          startIndex = index;
          console.log(startIndex);
        }

      });

      if( endIndex !== null ) {
        if (this.direction === "topBottom") {
          for (let i = startIndex; i <= endIndex; i++ ) {
            this.selectedArr.push(i+1);
          }
        } else {
          for (let i = startIndex; i >= endIndex; i-- ) {
            this.selectedArr.push(i+1);
          }
        }
      }

      this.$emit("change", this.selectedArr);
    },

    canvasMethod: function () {
      /* This method is used to show a rectangle when user drags on page. It also could understand that the user is near the top or bottom of page, and then it scrolls the page when the user is dragging. */
      const { myCanvas } = this.$refs;
      myCanvas.width = this.widthContainer;
      myCanvas.height = this.heightContainer;
      const html = document.documentElement;

      let ctx = myCanvas.getContext('2d');

      this.rect.startX = this.startDragData.x - myCanvas.offsetParent.offsetLeft;
      this.rect.startY = this.startDragData.y - myCanvas.offsetParent.offsetTop;

      this.rect.w = (this.whileDragData.x - myCanvas.offsetParent.offsetLeft) - this.rect.startX;
      this.rect.h = (this.whileDragData.y - myCanvas.offsetParent.offsetTop) - this.rect.startY ;

      if ( Math.abs(this.whileDragData.CLY - window.innerHeight) <  12) {
        console.log("near");
        html.scrollTop += 25;
      }
      if ( Math.abs(this.whileDragData.CLY) < 12 ) {
        html.scrollTop -= 25;
      }

      if ( (this.whileDragData.y > (myCanvas.offsetParent.offsetTop + myCanvas.offsetHeight) - 25) || (this.whileDragData.y < myCanvas.offsetParent.offsetTop + 25) ) {
        ctx.clearRect(0,0,myCanvas.width,myCanvas.height);
      }

      ctx.clearRect(0,0,myCanvas.width,myCanvas.height);
      ctx.setLineDash([6]);
      ctx.strokeRect(this.rect.startX, this.rect.startY, this.rect.w, this.rect.h);

    },

  },

  mounted() {
    this.childrenInfo();
  }
}
</script>

<style scoped>
.minHr {
  min-height: 900px;
}

#parentAll {
  position: relative;
}

#parentAll canvas {
  position: absolute;
  top: 0;
  left: 0;
}
</style>

I used a <canvas> to draw rectangle when the user is dragging. The main difference of my code with your is that it shows the selected items after the dragging process was finished. It works in both upward dragging and downward dragging and also when the user is want to continue dragging beyond the window area (scrolling).

Solution 2:[2]

I recommend you use DragSelect js library.

Working Demo

https://codesandbox.io/s/select-forked-tnmnwk?file=/src/components/HelloWorld.vue

mounted() {
  const vm = this;

  const ds = new DragSelect({
    selectables: document.querySelectorAll(".selectable-nodes"),
    area: document.getElementById("area"),
    draggability: false,
  });

  ds.subscribe("elementselect", function ({ item }) {
    vm.selectedItems.push();
  });

  ds.subscribe("elementunselect", function ({ item }) {
    const index = vm.selectedItems.indexOf(item.getAttribute("customAttribute"));
    if (index > -1) {
      vm.selectedItems.splice(index, 1);
    }
  });
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 hamid-davodi
Solution 2 User863