Mastering the art of scrolling and selecting items concurrently using the mouse in Vue

Struggling with a fascinating challenge of scrolling while selecting items using mouse drag in both up and down directions.

Here's a screenshot for reference:

https://i.stack.imgur.com/giMwY.png

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

The logic for drag selection is implemented in the Drag-select.vue file.

This triggers the change event when files are selected.

I handle the change event like this:

<drag-select-container @change="dragSelect($event)">

Update 1: per IVO GELO's comment

I've inserted this inside the 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);
    } 

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

This problem has been intriguing and complex, so any assistance would be greatly appreciated. Thank you in advance!

Answer №1

I suggest utilizing the DragSelect JavaScript library.

Take a look at the 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);
    }
  });
}

Answer №2

After working on your query, I managed to come up with a different approach for your code. The solution involves rewriting your code in a new and innovative way. Check out this demo where you can test it out. Essentially, the demo consists of two key components: the Parent component named "HelloCompo", and its corresponding code is as shown below:

HelloCompo:

<template>
  <!-- This component incorporates the "MyDrag" element -->
  <div id="wrapping_container" class="hello">
    <!-- Inserting the "MyDrag" element which triggers a custom "change" event upon user drag selection changes -->
    <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 utilizes data returned by the "MyDrag" element to establish the max and min range for accepting the "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 sets the "selectedItems" data after each change in the selected drag process.
      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>

There's also a child component known as "MyDrag":

MyDrag:

<template>
  <section id="parentAll">
    <div class="minHr" ref="container" @mousedown="startDrag" @mouseup="endDrag" @mousemove="whileDrag">
      <slot></slot>
    </div>

    <canvas ref="myCanvas" v-if="showCanvas" @mouseup="endDrag" @mousemove="whileDrag"></canvas>

  </section>
</template>

<script>
export default {
  name: "MyDrag",
  data() {
    return {
      dragStatus: false,
      childrenArr: [],
      startEvent: null,
      endEvent: null,
      direction: "topBottom", 
      selectedArr: [],
      heightContainer: null,
      widthContainer: null,
      /* Rectangular area data for drawing during drag */
      rect: {
        startX: null,
        startY: null,
        w: null,
        h: null
      },
      startDragData: {
        x: null,
        y: null
      },
      whileDragData: {
        x: null,
        y: null,
        CLY: null
      },
      showCanvas: false
    }
  },
  methods: {
    childrenInfo: function () {
      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) {
      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) {
      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) {
      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.showCanvas = true;
    },

    calculateDirection: function () {
      if (this.startEvent <= this.endEvent) {
        this.direction = "topBottom";
      } else {
        this.direction = "bottomTop";
      }
    },

    calculateSelected: function () {
      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 () {
      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>

Incorporating a <canvas> for drawing rectangles during dragging enhances the functionality. Interestingly, my code showcases the selected items post the drag completion. It caters to both upward and downward dragging scenarios, addressing additional features like continued dragging beyond window boundaries (scrolling support).

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Where is the first next() call's argument located?

I'm facing an issue with a simple generator function function *generate(arg) { console.log(arg) for(let i = 0; i < 3;i++) { console.log(yield i); } } After initializing the generator, I attempted to print values in the console: var gen ...

Utilizing a mouse-over script for image popup alignment

I recently came across the mouse over script below on a website, and I must say it works really well and is very easy to use. The only issue I have with it is the placement of the image. Currently, the image follows the cursor as it moves. Question: Is th ...

Enhance an item by introducing additional properties derived from its current key-value pairs

I have a task of converting the following object: myObj = { "pageName": "home", "dataExtract": "data1|data2=value2|data3=value3|data4=value4a,value4b,value4c"} Into this format: myObjMod = { 'pageName': 'home', 'dataExtract&apos ...

`javascript pop up notification will only display one time`

I am currently developing a chrome extension with two different modes. When the user clicks on the icon, a pop-up message should appear to indicate which mode is active. However, I am facing an issue where the message does not always display when the pop- ...

Saving information from JSON data obtained through the Google People API

My server is connected to the Google People API to receive contact information, and the data object I receive has a specific structure: { connections: [ { resourceName: 'people/c3904925882068251400', etag: '%EgYBAgkLNy4aDQECAwQFBgcICQoLD ...

Enhancing the default vue-cli webpack app with additional components

To ensure reproducibility, I began by setting up a basic project using vue-cli: npm install -g vue-cli vue init webpack spa cd spa npm install npm run dev Afterwards, I decided to swap out the Vue logo with an app-header component. Here's how I impl ...

How can I set a default value for a v-select using a function in vue.js / vuetify?

I'm new to utilizing vuetify and I am curious if there is a method to set a value for a v-select using a function. My form can create an enterprise, leveraging apollo.js to connect with the database. Although all fields populate correctly when modifyi ...

Is there a way to enlarge an iFrame to fill the entire screen with just a button click, without using JavaScript

I am currently attempting to achieve full screen mode for an iFrame upon clicking a button, similar to the functionality on this site. On that website, there is a bar that expands to full screen along with the embedded game, which is what I am aiming for. ...

Error encountered with MobileFirst version 8 and Angular JS v1.5.3 integration using Bootstrap

I am encountering an issue with my Cordova application that integrates with MobileFirst Platform version 8, Ionic version 1.3.1, and AngularJS version 1.5.3. Upon bootstrapping AngularJS to connect the app to MobileFirst Platform, I encounter the following ...

Adding options to a dropdown list dynamically within a ASP.NET Datagrid using Jquery

While the function in my 'aspx' page is functioning properly, I am facing an issue with the 'dropdown' inside the GridControl. It appears to be getting duplicated from the original row and not updating with the values from the appended ...

Validation with vee-validate performed instantly after inputting every character

Is it possible to validate after each input character instead of only upon clicking somewhere? Please enter your password: <div class='form-group' :class='{"form-group_invalid": errors.has("password") && error ...

Clicking on the current component should trigger the removal of CSS classes on its sibling components in React/JSX

Currently, I am working on creating a navigation bar using React. This navigation bar consists of multiple navigation items. The requirement is that when a user clicks on a particular navigation item, the class 'active' should be applied to that ...

Addressing three visual problems with an html form input, dropdown menu, and clickable button

Currently, the output of my code looks like this: https://i.stack.imgur.com/pl5CJ.jpg The first box (squared) represents an <input>, while the second box (rectangled) is a <select>. There are several issues that I am facing: The text entered ...

I created a custom discord.js-commando command to announce all the channels that my bot is currently active in, however, encountered an unexpected error

const Commando = require('discord.js-commando'); module.exports = class AnnounceCommand extends Commando.Command { constructor(client) { super(client, { name: 'announce', aliases: ['an'], ...

No data provided in nested parameters for Ajax post request

I am attempting to send an Ajax request to my rails controller using Ajax. $.ajax({ url: '/vulnerabilities/export', type: 'GET', processData: false, data: {export: {ids: this.refs.table.state.selectedRowKeys }} }); When there ...

The functionality of react-waypoint's onEnter/onLeave event handlers seems to be malfunctioning

Recently, I experimented with react-waypoint to control the visibility of a div. The code works as intended by hiding the div when it reaches the waypoint inside onEnter. When the div is inside, the isInView state becomes true, which in turn triggers the d ...

Modified the object path and updated the Dirty stages for nested objects within Vue state

Imagine having an object that contains arrays of objects in some properties. Whenever changes are made to any field, whether it's within an object in an array or the main object itself, you want to mark that object as "dirty" using a code snippet like ...

Exploring the Possibilities: Incorporating xlsx Files in Angular 5

Is there a way to read just the first three records from an xlsx file without causing the browser to crash? I need assistance with finding a solution that allows me to achieve this without storing all the data in memory during the parsing process. P.S: I ...

Is there a way for me to retrieve the variable from one function and use it in another

I have a tool for managing images with descriptions that allows me to edit both the text and the image file. The challenge is saving the modifications I make in my database. Currently, I can only save changes if I modify the image itself. If I only update ...

Using Environment Variables in Nuxt

Currently, I am utilizing the asyncData feature along with axios to fetch a local.json file from the static folder. At the moment, I only need to fetch it locally as I have included all my methods while waiting for the API to be developed. In order to use ...