What is the name of this 'effect' of selecting objects and how to do it in pure JS?

Asked

Viewed 1,202 times

19

I wanted to know what is the name of this 'effect' ('-') that serves to select folders and files in Windows Explorer.

ssssssssss


And if possible wanted to know how to do this with pure Javascript.

2 answers

35


Step 1 - Creating the "rectangle selector"

The main characteristic of this "effect" is to have a rectangle created during the selection process to demarcate the area being selected. It has one vertex at the starting point of the click and the other that accompanies the cursor, and is only visible while holding the mouse.

First we create a <div> invisible and apply some CSS to give the desired look:

<div id="selection"></div>
#selection {
    display: none;
    position: absolute;
    background: lightblue;
    border-color: blue;
    border-width: 1px;
    border-style: solid;
    opacity: .5;
}

Having this we will base ourselves on events onmousedown, onmousemove and onmouseup to create the behavior. Follow the code:

(function() {
    var beginX, beginY; // a posição do vértice fixo
    var active; // se a seleção está ativa (visível)
    var selection = document.getElementById("selection"); // o elemento

    window.onmousedown = function (e) {
        beginX = e.clientX;
        beginY = e.clientY;
        active = true;
        selection.style.display = "block"; // deixar a div visível
        window.onmousemove(e); // forçar a atualização de posição (função abaixo)
    };

    window.onmousemove = function (e) {
        if (active) {
            // cx,cy = a posição do segundo vértice
            var cx = e.clientX;
            var cy = e.clientY;

            // x,y,w,h = o retângulo entre os vértices
            var x = Math.min(beginX, cx);
            var y = Math.min(beginY, cy);
            var w = Math.abs(beginX - cx);
            var h = Math.abs(beginY - cy);

            // aplicar a posição e o tamanho
            selection.style.left = x+"px";
            selection.style.top = y+"px";
            selection.style.width = w+"px";
            selection.style.height = h+"px";
        }
    };

    window.onmouseup = function (e) {
        active = false; // desligar
        selection.style.display = "none"; // e ocultar
    };
})();

To keep the cursor stable as an arrow and avoid changing to others during selection (try clicking and dragging on an empty page, it changes), I will use this CSS. It’s not exactly pretty, but it works:

* {
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    cursor: default;
}

Here is the result so far: Jsfiddle.

Step 2 - Creating selectable items

Now we have a rectangle as desirable. But it has no function at all for now. We have to create selectable things. Let’s create some:

<div class="selectable">A</div>
<div class="selectable">B</div>
<div class="selectable">C</div>
<div class="selectable">D</div>
<div class="selectable">E</div>
<div class="selectable">F</div>
<div class="selectable">G</div>
<div class="selectable">H</div>
.selectable {
    font-size: 3em;
    background-color: lightgray;
    border-radius: 10px;
    display: inline-block;
    width: 100px;
    line-height: 100px;
    margin: 10px;
    text-align: center;
}

This should create a lot of comic books with letters. Our targets. To make the selection interact with them just that every time the mouse is moved check which items the selection rectangle overlaps. All that needs to be done is a rectangle collision check inside a loop. To get the rectangle of each item is used the function getBoundingClientRect(). At the end of the function onmousemove:

// procurar elementos selecionados
var list = document.getElementsByClassName("selectable");
for (var i = 0; i < list.length; ++i) {
    var rect = list[i].getBoundingClientRect();
    if (rect.bottom > y && rect.top < y+h && rect.right > x && rect.left < x+w) {
        list[i].classList.add("mark");
    }
    else {
        list[i].classList.remove("mark");
    }
}

And in the onmouseup unmark what was left marked:

// desmarcar tudo.
// aqui você pode fazer algo diferente manter marcado
var list = document.getElementsByClassName("selectable");
for (var i = 0; i < list.length; ++i) {
     list[i].classList.remove("mark");
}

This is the point where you can do something with the selection, like keep the items marked for example.

Of course, the class .mark so that we can see the effect:

.selectable.mark {
    background-color: yellow;
}

Once again: Jsfiddle.

Step 3 - Limiting a container

Now maybe we don’t want the whole page to be a selection area, but limit all of that to a single element. Let’s define a container:

<div id="container">
    <div id="selection"></div>

    <div class="selectable">A</div>
    <div class="selectable">B</div>
    <div class="selectable">C</div>
    <div class="selectable">D</div>
    <div class="selectable">E</div>
    <div class="selectable">F</div>
    <div class="selectable">G</div>
    <div class="selectable">H</div>
</div>
#container {
    margin: 40px;
    padding: 10px;
    border-style: solid;
}

For this to work we will use again the getBoundingClientRect() and take a limit rectangle.

var limit = document.getElementById("container").getBoundingClientRect();

First, add to the beginning of onmousedown so that nothing happens if the click was out of limit:

// se o clique foi fora do limite, não continuar
if (e.clientX > limit.right || e.clientX < limit.left ||
    e.clientY > limit.bottom || e.clientY < limit.top) {
    return;
}

As a last modification, make the second vertex of the selection (the one that is mobile and follows the cursor) never leave the limit. No onmousemove:

var cx = Math.max(Math.min(e.clientX, limit.right), limit.left);
var cy = Math.max(Math.min(e.clientY, limit.bottom), limit.top);

The result so far: Jsfiddle.

Step 4 - The Evil Scroll

The problem so far is that you put enough items to cause an overflow and create scroll, it will not be taken into account and the selection will happen in the wrong position.

This happens because the mouse position is returned relative to the screen and not to the point 0,0 of the page. Also the getBoundingClientRect() refers to the screen. So we do not need to touch the check of the onmousedown, but we need to set correct values in the beginX/Y. So:

beginX = e.clientX + document.body.scrollLeft;
beginY = e.clientY + document.body.scrollTop;

Already in the onmousemove we have some changes to make. First create variables for the actual mouse position, referring to the body and not to the canvas:

var sx = document.body.scrollLeft;
var sy = document.body.scrollTop;

var mx = e.clientX + sx;
var my = e.clientY + sy;

And use these variables in the vertex calculation:

var cx = Math.max(Math.min(mx, limit.right), limit.left);
var cy = Math.max(Math.min(my, limit.bottom), limit.top);

Now x,y,w,h will be correct, but the comparison that checks which items are selected will fail because getBoundingClientRect() still refers to the screen. Just correct the measures on the conditional:

var rect = list[i].getBoundingClientRect();
if (rect.bottom+sy > y && rect.top+sy < y+h && rect.right+sx > x && rect.left+sx < x+w) {
    list[i].classList.add("mark");
}
else {
    list[i].classList.remove("mark");
}

The final result: Jsfiddle.

  • =The/ if possible I wanted the 'Selection' to select Ivs, as shown in the image there, if possible :D

  • 1

    Now you have the selection rectangle and the rectangle of each div. Just calculate a collision of rectangles within the onmousemove for each "selectable" div. The basic idea is this.

  • 1

    ah right, vlw the/

  • Could you update your example to select Divs? rsrsrs failed to do =/

  • 1

    @Iagobruno added an example for this too

  • Thank you @Guilhermebernal Õ/

  • Just one thing, I want you to only select elements within a tag, in case I start selecting outside the tag, it won’t work, I tried it but it didn’t work very well, I used the method e.target to check, but it checks the element clicked or if I click on some element inside the tag it will send the tag clicked not the tag I want '-' I think you understand kkkkk

  • @Iagobruno added this too. Take a look.

  • @Guilhermebernal opened the 3 Jsfiddle’s and none of them worked I’m using Mozilla Firefox 27.0

  • @Pauloroberto I updated all three. The problem is that firefox does not define the e.x, different from Chrome. I used e.clientX, that both browsers define.

  • 1

    dude! turned out really good ! : ) @Guilhermebernal I didn’t know I had how to do it so easy I thought it was extremely more complex ! :)

  • 1

    @Guilhermebernal Your example was the best I found on the internet, but since the day you answered the question I’ve been trying to make it work with scroll in other =/positions as well. If it wasn’t too much to ask =p I wanted you to adapt so that the scroll bar wouldn’t affect the functionality. It would have like?

  • 1

    @Iagobruno I finished rewriting everything (the answer ended up having enough visibility). See now :)

  • Thanks, but the scroll went wrong =/

  • 1

    @Iago Can you give more details of "didn’t work"? Which browser?

  • 1

    I was able to fix it, you just replaced Scrollleft with Top and Top with left. http://jsfiddle.net/qnN55/2/

Show 11 more comments

5

I got the answer from @Guilherme Bernal (more precisely the example of selecting div’s) and incremented so that they remain selected in the event mouseup.

Example: Jsfiddle

The changes were:

  • 1 - a new function to check if an index exists in an array (it is fed as user selects div’s);
  • 2 - Small implementations in existing methods according to the his answer;
  • 3 - The new variable "var selecteds = [];".

window.onmousedown = function (e) {
    [...]
    selecteds = [];
};

window.onmousemove = function (e) {
    [if->for]
            var rect = list[i].getBoundingClientRect();
            if (rect.bottom > y && rect.top < y+h &&
                rect.right > x && rect.left < x+w) {
                list[i].style.backgroundColor = "red";
                selecteds.push(list[i]);
            } else {
                list[i].style.backgroundColor = "lightgreen";
            }
    [/if->/for]
};

window.onmouseup = function (e) {
    [...]
    for (var i = 0; i < list.length; ++i) {
        if(selecteds.length > 0){
            index = list.indexOf(i);
            if (index > -1) {
                array.splice(index, 1);
            }
        }
        if(indexOf.call(selecteds, list[i])){return;}
        list[i].style.backgroundColor = "lightgreen";
    }
};

Browser other questions tagged

You are not signed in. Login or sign up in order to post.