[How To] Connect Elements With Lines on Web Page
Tags: javascript, css, html, svg
Published: 2019-08-28 12:55:42

Recently I had a project for Institute of Textbooks where I had to make an WEB application with tasks from their 5th grade textbook. There was nine types of tasks and one of them was to connect words(or sentences) with lines. I knew that HTML has no native support for this kind of stuff so I had to improvise somehow. Of course that first thing that I've done was to look for some JS library but anything that I could find was not lightweight and has a lot more features that I needed. Also this WEB application should be responsive and supported on touch devices and older browsers(latest versions of Chrome and Firefox supported by Windows XP(don't ask...)).

###Sneak peak of final result ✅
Here you can see the final result how it looks when you connect some words with another and check if connections are correct.
![Sneak peak of final result](https://thepracticaldev.s3.amazonaws.com/i/xfhnteflc26e1ubbsku0.gif)
###The idea 💡
At first I though about using div's with absolute position, 2-3px height and dynamical width(calculated distance between two hooks) and also rotation with rotation origin in the left top(or bottom), but that was just awful.

Two minutes later I thought about canvas, we all know that canvas should be used for drawings like this but canvas has one(well actually probably many but one in this case) drawback, it's just drawing and we cannot modify elements when already drawn(we can, but then we must redraw entire canvas).

**SVG**. **S**calable **V**ector **G**raphics. This is the answer. Main difference between Canvas and SVG is that Canvas is bitmap(pixels and colors) and SVG keeps all his elements in HTML DOM. So if you want graphics intensive stuffs you should use Canvas, and if you want graphics with ability to modify elements and you will not have a lot of them(because it will affect performance drastically) then you should use SVG.
###But, how? 🤔
I have to mention that I didn't use exact this code in my project, I'm posting simplified version so you can get an idea and implement as you want.

Okay, at this point we know that we'll use SVG for drawing lines and other content will be plain HTML. In order to achieve what we want, we will make structure like this
```html
<div class="wrapper">
  <svg></svg>
  <div class="content">
    <ul>
      <li>One <div class="hook" data-value="One" data-accept="First"></div></li>
      <li>Two <div class="hook" data-value="Two" data-accept="Second"></div></li>
      <li>Three <div class="hook" data-value="Three" data-accept="Third"></div></li>
    </ul>
    <ul>
      <li><div class="hook" data-value="Second" data-accept="Two"></div> Second</li>
      <li><div class="hook" data-value="Third" data-accept="Three"></div> Third</li>
      <li><div class="hook" data-value="First" data-accept="One"></div> First</li>
    </ul>
  </div>
</div>
```
As you can see, I'm using datasets to describe my hooks(points for drawing and attaching corresponding lines).

And some CSS to arrange content properly
```css
.wrapper {
  position: relative;
}
.wrapper svg {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  shape-rendering: geometricPrecision; /* for better looking lines */
}
.wrapper .content {
  position: relative;
  z-index: 2;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
}
.wrapper .hook {
  background-color: blue;
  display: inline-block;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  cursor: pointer;
}
```
Now we have all set up and it's time for some JavaScript.
```javascript
const wrapper = document.querySelector(".wrapper")
const svgScene = wrapper.querySelector("svg")
const content = wrapper.querySelector(".content")

const sources = []
let currentLine = null
let drag = false
```
<code>sources</code> will contain lines with their start and end hooks, in <code>currentLine</code> we'll store current line we drawing and <code>drag</code> will tell us if we are currently drawing a new line.

As I mentioned before, this code should work both on desktop and mobile(touch) devices so I had to write code which will work in both cases.

First we will attach event listeners
```javascript
wrapper.addEventListener("mousedown", drawStart)
wrapper.addEventListener("mousemove", drawMove)
wrapper.addEventListener("mouseup", drawEnd)

wrapper.addEventListener("touchstart", drawStart)
wrapper.addEventListener("touchmove", drawMove)
wrapper.addEventListener("touchend", drawEnd)
```
See that I'm using same methods for mouse and touch events.
###drawStart()
Since this method is attached on wrapper and not on hook, first thing we should do is to check if user has started drawing line from correct point
```javascript
if(!e.target.classList.contains("hook")) return
```
Second thing is to capture mouse(or touch) X and Y coordinates
```javascript
let eventX = e.type == "mousedown" ? e.clientX - scene.offsetLeft : e.targetTouches[0].clientX - scene.offsetLeft
let eventY = e.type == "mousedown" ? e.clientY - scene.offsetTop + window.scrollY : e.targetTouches[0].clientY - scene.offsetTop + window.scrollY
```
And to draw a line
```javascript
let lineEl = document.createElementNS('http://www.w3.org/2000/svg','line')
currentLine = lineEl;
currentLine.setAttribute("x1", eventX)
currentLine.setAttribute("y1", eventY)
currentLine.setAttribute("x2", eventX)
currentLine.setAttribute("y2", eventY)
currentLine.setAttribute("stroke", "blue")
currentLine.setAttribute("stroke-width", "4")

svgScene.appendChild(currentLine)
sources.push({ line: lineEl, start: e.target, end: null })

drag = true
```
Hey but we don't have second point coordinates?!?! Yep, that's right, that's where <code>drawMove()</code> kicks in. You see that we set our <code>drag</code> flag to <code>true</code>.
###drawMove()
This method is invoked when user moves mouse(or touch) on our wrapper element, so first thing we have to do is to check if user is drawing a line or just moving his mouse(touch)
```javascript
if (!drag || currentLine == null) return
```
Second thing here is the same as from <code>drawStart()</code>
```javascript
let eventX = e.type == "mousedown" ? e.clientX - scene.offsetLeft : e.targetTouches[0].clientX - scene.offsetLeft
let eventY = e.type == "mousedown" ? e.clientY - scene.offsetTop + window.scrollY : e.targetTouches[0].clientY - scene.offsetTop + window.scrollY
```
And finally we update second point coordinates of line
```javascript
currentLine.setAttribute("x2", eventX)
currentLine.setAttribute("y2", eventY) 
```
At this stage you will have your scene with hooks and you'll be able to draw line with one point attached on hook and second point following your mouse(or touch) until you release your mouse button(or move your finger from screen) and line will freeze. Let's move on next method.
###drawEnd()
This method is invoked when user release mouse button or move his finger off screen, so first we have to ensure that he's been drawing a line
```javascript
if (!drag || currentLine == null) return
```
Second thing is to define our <code>targetHook</code>
```javascript
let targetHook = e.type == "mouseup" ? e.target : document.elementFromPoint(e.changedTouches[0].clientX, e.changedTouches[0].clientY)
```
See that I used <code>e.target</code> for mouseup event and <code>document.elementFromPoint()</code> for touch devices to get <code>targetHook</code>? That's because <code>e.target</code> in <code>mouseup</code> event will be element we currently hovering and in <code>touchend</code> event it will be element on which touch started.

What if user want to attach end of line on element which is not hook or to hook where line started? We will not allow that.
```javascript
if (!targetHook.classList.contains("hook") || targetHook == sources[sources.length - 1].start) {
  currentLine.remove()
  sources.splice(sources.length - 1, 1)
} else {
  // patience, we'll cover this in a second
}
```
And finally if the end of the line is on correct position
```javascript
if (!targetHook.classList.contains("hook") || targetHook == sources[sources.length - 1].start) {
  currentLine.remove()
  sources.splice(sources.length - 1, 1)
} else {
  sources[sources.length - 1].end = targetHook

  let deleteElem = document.createElement("div")
  deleteElem.classList.add("delete")
  deleteElem.innerHTML = "&#10005"
  deleteElem.dataset.position = sources.length - 1
  deleteElem.addEventListener("click", deleteLine)
  let deleteElemCopy = deleteElem.cloneNode(true)
  deleteElemCopy.addEventListener("click", deleteLine)

  sources[sources.length - 1].start.appendChild(deleteElem)
  sources[sources.length - 1].end.appendChild(deleteElemCopy)
}

drag = false
```
Now we have to implement <code>deleteLine()</code> method to allow our user to delete line.

First some CSS
```css
.wrapper .hook > .delete {
  position: absolute;
  left: -3px;
  top: -3px;
  width: 21px;
  height: 21px;
  background-color: red;
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 50%;
}
.wrapper .hook:hover {
  transform: scale(1.1);
}
```
and implementation of <code>deleteLine()</code>
```javascript
let position = e.target.dataset.position

sources[position].line.remove();
sources[position].start.getElementsByClassName("delete")[0].remove()
sources[position].end.getElementsByClassName("delete")[0].remove()
sources[position] = null
```
And what about checking if words are connected properly?
Method <code>checkAnswers()<code>
```javascript
sources.forEach(source => {
  if (source != null) {
    if (source.start.dataset.accept.trim().toLowerCase() == source.end.dataset.value.trim().toLowerCase() && source.end.dataset.accept.trim().toLowerCase() == source.start.dataset.value.trim().toLowerCase()) {
      source.line.style.stroke = "green"
    } else {
      source.line.style.stroke = "red"
    }
  }
})
```
###THE END 🎉
That's all, now you have fully implemented drag'n'draw line functionality with minimum use of uncommon html tags and best of all, it works both on non-touch and touch devices!

I hope you liked this article and learned something new 😊