1


I'm creating a little graphic editor, where the user (by now) could insert some "symbols", that consist of some svg-elements, grouped inside a g-tag.
Additionally, he could draw lines in different colors yet.

By now I am able to select single drawn lines and Symbols and I also could select more objects by clicking on them, while Holding the Control-key. (For those, who are interrested in it, a selected object gets a class "selected", so I could find them programatically by d3.select('.selected').)

My new Goal ist to draw a rectangle with the mouse over such Elements and select the Elements inside the rectangle.
For this, I catch the pointerdown-event, where I add a rectangle to the svg-box and scale it inside pointermove-event.
Attached, a simple Video of my actual Version.

I have two Questions by now:

1) How can I avoid that the Elements are higlited like selected text while moving the mouse with pressed left button? (you can see the flickering in the Video)
Is there perhaps something like event.preventDefault(); to do so?

2) ...and that is the greater problem…
Is drawing a rectangle a good way to do this and how can i quickly calculate which elements are inside this rectangle? Is there perhaps a specialized function in d3, that I didn't find yet?

EDIT: for clarification, I attached a screenshot of the svg-structur of a Symbol and a line: Sample

Simple sample video

CodePen example: https://codepen.io/Telefisch/pen/LoEReP

$(document).ready(function () {  
svgDrawing = document.getElementById('drawing');
  svgDrawing.addEventListener('pointerdown', mouseButtonPressed);
  svgDrawing.addEventListener('pointerup', mouseButtonReleased);
  svgDrawing.addEventListener('pointermove', mouseMove);
}) ...

Additional question:
What's the difference between
svg_children[i].className.baseVal += ' selected'; and
svg_children[i].classList.add('selected')
I have some problems that baseVal seems not to be stored inside the dom? If I use it that way, I couldn't see the class in the elements-pane of the developer-window, but it pops up at the symbol. If I use ClassList.add, I can see the class in the Elements-Pane also.
Screenshot:Screenshot
As you can see, the yellow-marked seems to have the class in the popup but not in the Elements-code. This is added by svg_children[i].className.baseVal += ' selected';
The red-marked 'selected'-class was added by svg_children[i].classList.add('selected')


Thanks so far,
Carsten

Telefisch
  • 305
  • 1
  • 19
  • 1
    Could you share a minimal reproducible example? i.e. a CodePen or JSFiddle etc.? – Alex L Jan 24 '20 at 12:07
  • Hmm… I don't know exactly, what you want to see? Of Course I could create some simlyfied example but what do you hope to see? – Telefisch Jan 28 '20 at 09:44
  • In general, a simplified example in a CodePen type sandbox to look at and to solve that way is best - https://stackoverflow.com/help/how-to-ask even if it is a simplified or anonymized version of your app, then we can provide a concrete answer. If you add real code then we know what you have tried so far, can reproduce it, can test it and can solve it. – Alex L Jan 28 '20 at 10:01
  • I will do my best, only tooks an hour or so. I don't get Java to run on codepen. – Telefisch Jan 28 '20 at 10:20
  • Why does this not work in Codepen?: `$(document).ready(function () { console.log("hier"); })` `https://code.jquery.com/jquery-2.2.4.min.js` already added. – Telefisch Jan 28 '20 at 10:29
  • Works for me: https://codepen.io/Alexander9111/pen/MWYRzqj (added https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js) – Alex L Jan 28 '20 at 10:40
  • Seems to be an Edge Problem. Chrome works. Now I will create an example. – Telefisch Jan 28 '20 at 11:01
  • Ok, here it is: [link](https://codepen.io/Telefisch/pen/LoEReP) in chrome the problem with the flickering doesn't exist so I only need a hint to calculate the elements inside the rectangle. – Telefisch Jan 28 '20 at 11:23
  • check out my answer and demo: https://codepen.io/Alexander9111/pen/XWJQoPP – Alex L Jan 28 '20 at 13:57

1 Answers1

1

I think I have a solution for you, using .getClientBoundingRect() of the svg elements and the <rect.selectionBox/> to find out if your box is overlapping them etc.

Demo - https://codepen.io/Alexander9111/pen/XWJQoPP:

enter image description here

Code:

var svgDrawing = document.getElementById('drawing');
var pointerOrigin;
var point = svgDrawing.createSVGPoint();
var drawRectToSelect
var raster = 10;

$(document).ready(function () {
  svgDrawing = document.getElementById('drawing');
  svg_rect = svgDrawing.getBoundingClientRect();
  console.log("svg_rect", svg_rect);
  g = document.getElementById("437");
  //svg_children = g.childNodes;
  svg_children = g.querySelectorAll("*");
  console.log(svg_children);
  svgDrawing.addEventListener('pointerdown', e => mouseButtonPressed(e));
  svgDrawing.addEventListener('pointerup', e => mouseButtonReleased(e));
  svgDrawing.addEventListener('pointermove', e => mouseMove(e));
})

function mouseButtonPressed(evt) {
  pointerOrigin = getPointFromEvent(evt);
  if(evt.button === 0)
    {
      drawRectToSelect = d3.select('#drawing')
      .append('rect')
      .attr("id","temp_selection")
      .classed('selectionBox', true)
      .attr("x", Math.round(pointerOrigin.x / raster) * raster)
            .attr("y", Math.round(pointerOrigin.y / raster) * raster)
            .attr("height", raster)
            .attr("width", raster);
    }
}

function mouseMove(evt) {
    if (!drawRectToSelect) { return; }

    evt.preventDefault();  //Verschieben der gesamten Seite unterbinden

    var pointerPosition = getPointFromEvent(evt);
    if (drawRectToSelect) {
        drawRectToSelect
            .attr("width", Math.round((pointerPosition.x - pointerOrigin.x) / raster) * raster)
            .attr("height", Math.round((pointerPosition.y - pointerOrigin.y) / raster) * raster);
    }
}

function elementIsInside(el, box){
  var result = false;
  el_rect = el.getBoundingClientRect();
  box_rect = box.getBoundingClientRect();
  // console.log("rects_" + el.tagName, el_rect, box_rect)
  // console.log("rects_" + el.tagName, el, box)
  if (el_rect.right >= box_rect.left && el_rect.right <= box_rect.right
     && el_rect.bottom >= box_rect.top && el_rect.bottom <= box_rect.bottom){
    result = true;
  } else if (el_rect.left >= box_rect.left && el_rect.left <= box_rect.right
     && el_rect.bottom >= box_rect.top && el_rect.bottom <= box_rect.bottom){
    result = true;
  } else if (el_rect.right >= box_rect.left && el_rect.right <= box_rect.right
     && el_rect.top >= box_rect.top && el_rect.top <= box_rect.bottom){
    result = true;
  } else if (el_rect.left >= box_rect.left && el_rect.left <= box_rect.right
     && el_rect.top >= box_rect.top && el_rect.top <= box_rect.bottom){
    result = true;
  }
  // console.log("result_" + el.tagName, result)
  return result;
}

function mouseButtonReleased(evt) {
    svgDrawing.style.cursor = null;

    if (drawRectToSelect) {
      const box = document.querySelector('#temp_selection');
      for (i=0; i < svg_children.length; i++){
        //svg_children[i].classList.add("selected");
        console.log(svg_children[i].tagName)
        console.log(svg_children[i].className.baseVal)
        child_rect = svg_children[i].getBoundingClientRect();
        console.log(child_rect);        

        //calculate elements inside rectangle
        if (elementIsInside(svg_children[i], box )){
          if (svg_children[i].className.baseVal.includes('selected')){

          } else {
            svg_children[i].className.baseVal += " selected";
            svg_children[i].className.animVal += " selected";
          }
        } else {          
          if (svg_children[i].className.baseVal.includes('selected')){
            console.log("true")
            svg_children[i].className.baseVal = svg_children[i].className.baseVal.replace(" selected"," ");
            svg_children[i].className.animVal = svg_children[i].className.animVal.replace(" selected"," ");
            console.log(svg_children[i].className.baseVal);
          } else {
            console.log("false")
            console.log(svg_children[i].className.baseVal);
          }
        }              
      }                  
      //Delete selection-rectangle
      drawRectToSelect.remove();
      drawRectToSelect = null;
    }
}

function getPointFromEvent(evt) {
    if (evt.targetTouches) {
        point.x = evt.targetTouches[0].clientX;
        point.y = evt.targetTouches[0].clientY;
    } else {
        point.x = evt.clientX;
        point.y = evt.clientY;
    }
    var invertedSVGMatrix = svgDrawing.getScreenCTM().inverse();

    return point.matrixTransform(invertedSVGMatrix);
}

Firstly, you have to pass in the event argument to use it later:

$(document).ready(function () {
  svgDrawing = document.getElementById('drawing');
  svg_rect = svgDrawing.getBoundingClientRect();
  console.log("svg_rect", svg_rect);
  g = document.getElementById("437");
  //svg_children = g.childNodes;
  svg_children = g.querySelectorAll("*");
  console.log(svg_children);
  svgDrawing.addEventListener('pointerdown', e => mouseButtonPressed(e));
  svgDrawing.addEventListener('pointerup', e => mouseButtonReleased(e));
  svgDrawing.addEventListener('pointermove', e => mouseMove(e));
})

Then I created a function which tests if the box overlaps at least 1 corner of the element's bounding box:

function elementIsInside(el, box){
var result = false;
  el_rect = el.getBoundingClientRect();
  box_rect = box.getBoundingClientRect();
  // console.log("rects_" + el.tagName, el_rect, box_rect)
  // console.log("rects_" + el.tagName, el, box)
  if (el_rect.right >= box_rect.left && el_rect.right <= box_rect.right
     && el_rect.bottom >= box_rect.top && el_rect.bottom <= box_rect.bottom){
    result = true;
  } else if (el_rect.left >= box_rect.left && el_rect.left <= box_rect.right
     && el_rect.bottom >= box_rect.top && el_rect.bottom <= box_rect.bottom){
    result = true;
  } else if (el_rect.right >= box_rect.left && el_rect.right <= box_rect.right
     && el_rect.top >= box_rect.top && el_rect.top <= box_rect.bottom){
    result = true;
  } else if (el_rect.left >= box_rect.left && el_rect.left <= box_rect.right
     && el_rect.top >= box_rect.top && el_rect.top <= box_rect.bottom){
    result = true;
  }
  // console.log("result_" + el.tagName, result)
  return result;
}

And this gets called from your function (and adds or removes the .selected class):

function mouseButtonReleased(evt) {
    svgDrawing.style.cursor = null;

    if (drawRectToSelect) {
      const box = document.querySelector('#temp_selection');
      for (i=0; i < svg_children.length; i++){
        //svg_children[i].classList.add("selected");
        console.log(svg_children[i].tagName)
        console.log(svg_children[i].className.baseVal)
        child_rect = svg_children[i].getBoundingClientRect();
        console.log(child_rect);        

        //calculate elements inside rectangle
        if (elementIsInside(svg_children[i], box )){
          if (svg_children[i].className.baseVal.includes('selected')){

          } else {
            svg_children[i].className.baseVal += " selected";
            svg_children[i].className.animVal += " selected";
          }
        } else {          
          if (svg_children[i].className.baseVal.includes('selected')){
            console.log("true")
            svg_children[i].className.baseVal = svg_children[i].className.baseVal.replace(" selected"," ");
            svg_children[i].className.animVal = svg_children[i].className.animVal.replace(" selected"," ");
            console.log(svg_children[i].className.baseVal);
          } else {
            console.log("false")
            console.log(svg_children[i].className.baseVal);
          }
        }              
      }                  
      //Delete selection-rectangle
      drawRectToSelect.remove();
      drawRectToSelect = null;
    }
}
Alex L
  • 4,168
  • 1
  • 9
  • 24
  • Looks pretty cool. I need some time to understand but first, you passed the e-parameter to the events. I didn't do this and it worked fine. Is there a difference? I'll be back with new questions after I understood completely, what you did, I think. – Telefisch Jan 28 '20 at 14:54
  • Ahh it seems the event parameter gets passed implicitly, I always passed it explicitly but this answer suggests you don't need to: https://stackoverflow.com/a/35936912/9792594 - Great to hear you like my answer. If you liked it and/or it helps you please consider marking it as the correct answer and up-voting it. Look forward to hearing any questions you have! – Alex L Jan 28 '20 at 15:23
  • I need to select a complete symbol (nested in the g-tag, classed "preview" so this example was a nice help to get this run. For the symbol-selection, I only had to change the svg_children-selection to document.getElementsByClassName('preview'). thank you, Carsten – Telefisch Jan 29 '20 at 08:05
  • Hi Alex, I added a screenshot and an additional question in my post. Could you please have a look at it? – Telefisch Jan 31 '20 at 11:50
  • 1
    You should use ``.classList.add()`` if it works for you. This is the prefered method but in the codePen, I was having errors such as "could not call .add method of null" and when I inspected the elements they had an object for the class rather than a simple string. Something like ``{baseVal: "some-class", AnimVal: "some-class"}`` so I figured it was something you set up. Like a base-class and animated-class. – Alex L Jan 31 '20 at 22:40
  • Ok, cool. Then I did it the right way. baseVal seems to be something temporary...? Whatever, it works with `.classList.add('classname')` Thanks again. – Telefisch Feb 03 '20 at 07:56
  • 1
    They should be only present when using ```` or ```` tags etc. (SMIL) - then baseVal is to hold the default value and animVal is to hold the current, animated value. https://stackoverflow.com/a/5889988/9792594 - I was also a bit confused as I did not see any of these animation tags in your HTML. Therefore, ``.classList.add()`` is definitely the best way to go if possible :) – Alex L Feb 03 '20 at 08:17