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

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. Scalable Vector Graphics. 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

  • One
  • Two
  • Three
  • Second
  • Third
  • First

``` 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 ``` sources will contain lines with their start and end hooks, in currentLine we'll store current line we drawing and drag 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 drawMove() kicks in. You see that we set our drag flag to true.

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 drawStart() 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 targetHook javascript let targetHook = e.type == "mouseup" ? e.target : document.elementFromPoint(e.changedTouches[0].clientX, e.changedTouches[0].clientY) See that I used e.target for mouseup event and document.elementFromPoint() for touch devices to get targetHook? That's because e.target in mouseup event will be element we currently hovering and in touchend 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 = "✕" 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 deleteLine() 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 deleteLine() ```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 😊