This content originally appeared on DEV Community and was authored by Jscrambler
The JavaScript language lives in a browser. Actually, let's rephrase that: a web browser has a separate part inside of it, called the JavaScript engine. This engine can understand and run JS code.
There are many other separate parts that, altogether, make up the browser. These parts are different Browser APIs, also known as Web APIs. The JS engine is there to facilitate the execution of the JS code we write. Writing JS code is a way for us (developers) to access various functionalities that exist in the browser and that are exposed to us via Browser APIs.
What Is a Web API?
Web APIs are known as "browser features". Arguably, the most popular of these various browser features—at least for JavaScript developers—is the browser console. The Console API allows us to log out the values of variables in our JavaScript code. Thus, we can manipulate values in our JS code and log out these values to verify that a specific variable holds a specific (expected) value at a certain point in the thread of execution, which is great for debugging. If you've spent any significant amount of time with the JS language, this should all be pretty familiar.
What some beginner JavaScript developers do not comprehend is the big picture of the browser having a large number of these "browser features" built-in—and accessible to us via various JavaScript “facade” methods: methods that look like they’re just a part of the language itself, but are actually “facades” for features outside of the JS language itself. Some examples of widely used Web APIs are the DOM API, the Canvas API, the Fetch API, etc.
The JavaScript language is set up in such a way that we can't immediately infer that the functionality we're using is in fact a browser feature. For example, when we say:
let heading = document.getElementById('main-heading');
... we're actually hooking into a browser feature—but there's no way of knowing this since it looks like regular JS code.
The Drag and Drop Web API
To understand how the Drag and Drop API works, and to effectively use it, all we have to do is know some basic concepts and methods it needs. Similar to how most front-end developers are familiar with the example from the previous section (namely, document.getElementById
), we need to learn:
- the basic concepts of the Drag and Drop Web API;
- at least a few basic methods and commands.
The first important concept related to the Drag and Drop API is the concept of the source and target elements.
Source and Target Elements
There are built-in browser behaviors that determine how certain elements will behave when a user clicks and drags them on the viewport. For example, if we try click-dragging the intro image of this very tutorial, we'll see a behavior it triggers: the image will be displayed as a semi-transparent thumbnail, on the side of our mouse pointer, following the mouse pointer for as long as we hold the click. The mouse pointer also changes to the following style:
cursor: grabbing;
We've just shown an example of an element becoming a source element for a drag-and-drop operation. The target of such an operation is known as the target element.
Before we cover an actual drag and drop operation, let's have a quick revision of events in JS.
Events in JS: A Quick Revision
We could go as far as saying that events are the foundation on which all our JavaScript code rests. As soon as we need to do something interactive on a web page, events come into play.
In our code, we listen for: mouse clicks, mouse hovers (mouseover events), scroll events, keystroke events, document loaded events...
We also write event handlers that take care of executing some JavaScript code to handle these events.
We say that we listen for events firing and that we write event handlers for the events being fired.
Describing a Drag and Drop Operation, Step By Step
The HTML and CSS
Let's now go through a minimal drag and drop operation. We'll describe the theory and concepts behind this operation as we go through each step.
The example is as easy as can be: there are two elements on a page. They are styled as boxes. The first one is a little box and the second one is a big box.
To make things even easier to comprehend, let's "label" the first box as "source", and the second one as "target":
<div id="source">Source</div>
<div id="target">Target</div>
<style>
#source {
background: wheat;
width: 100px;
padding: 20px;
text-align: center;
}
#target {
background: #abcdef;
width: 360px;
height: 180px;
padding: 20px 40px;
text-align: center;
margin-top: 50px;
box-sizing: border-box;
}
</style>
A little CSS caveat above: to avoid the added border width increasing the width of the whole target div
, we’ve added the CSS property-value pair of box-sizing: border-box
to the #target
CSS declaration. Thus, the target element has consistent width, regardless of whether our drag event handlers are running or not.
The output of this code is fairly straightforward:
Convert the “Plain” HTML Element Into a Drag and Drop Source Element
To do this, we use the draggable
attribute, like so:
<div id="source" draggable="true">Source</div>
What this little addition does is changing the behavior of the element. Before we added the draggable
attribute, if a user click-dragged on the Source div
, they'd likely just highlight the text of the div
(i.e the word "Source")—as if they were planning to select the text before copying it.
However, with the addition of the draggable
attribute, the element changes its behavior and behaves exactly like a regular HTML img
element — we even get that little grabbed
cursor—giving an additional signal that we've triggered the drag-and-drop functionality.
Capture Drag and Drop Events
There are 8 relevant events in this API:
- drag
- dragstart
- dragend
- dragover
- dragenter
- dragleave
- drop
- dragend
During a drag and drop operation, a number of the above events can be triggered: maybe even all of them. However, we still need to write the code to react to these events, using event handlers, as we will see next.
Handling the Dragstart and Dragend Events
We can begin writing our code easily. To specify which event we’re handling, we’ll just add an on
prefix.
For example, in our HTML code snippet above, we’ve turned a “regular” HTML element into a source element for a drag-and-drop operation. Let’s now handle the dragstart
event, which fires as soon as a user has started dragging the source element:
let sourceElem = document.getElementById('source');
sourceElem.addEventListener('dragstart', function (event) {
confirm('Are you sure you want to move this element?');
})
All right, so we’re reacting to a dragstart
event, i.e. we’re handling the dragstart
event.
Now that we know we can handle the event, let’s react to the event firing by changing the styles of the source element and the target element.
let sourceElem = document.getElementById('source');
let targetElem = document.getElementById('target');
sourceElem.addEventListener('dragstart', function (event) {
event.currentTarget.style = "opacity:0.3";
targetElem.style = "border: 10px dashed gray;";
})
Now, we’re handling the dragstart
event by making the source element see-through, and the target element gets a big dashed gray border so that the user can more easily see what we want them to do.
It’s time to undo the style changes when the dragend
event fires (i.e. when the user releases the hold on the left mouse button):
sourceElem.addEventListener('dragend', function (event) {
sourceElem.style = "opacity: 1";
targetElem.style = "border: none";
})
Here, we’ve used a slightly different syntax to show that there are alternative ways of updating the styles on both the source and the target elements. For the purposes of this tutorial, it doesn’t really matter what kind of syntax we choose to use.
Handling the Dragover and Drop Events
It’s time to deal with the dragover
event. This event is fired from the target element.
targetElem.addEventListener('dragover', function (event) {
event.preventDefault();
});
All we’re doing in the above function is preventing the default behavior (which is opening as a link for specific elements). In a nutshell, we’re setting the stage for being able to perform some operation once the drop
event is triggered.
Here’s our drop
event handler:
targetElem.addEventListener('drop', function (event) {
console.log('DROP!');
})
Currently, we’re only logging out the string DROP!
to the console. This is good enough since it’s proof that we’re going in the right direction.
Sidenote: notice how some events are emitted from the source element, and some other events are emitted from the target element. Specifically, in our example, the sourceElem
element emits the dragstart
and the dragend
events, and the targetElem
emits the dragover
and drop
events.
Next, we’ll use the dataTransfer
object to move the source element onto the target element.
Utilize The dataTransfer Object
The dataTransfer
object “lives” in an instance of the Event object—which is built-in to any event. We don’t have to “build” the event object—we can simply pass it to the anonymous event handler function—since functions are “first-class citizens” in JS (meaning: we can pass them around like any other value)—this allows us to pass anonymous functions to event handlers, such as the example we just saw in the previous section.
In that piece of code, the second argument we passed to the addEventListener()
method is the following anonymous function:
function(event) {
console.log('DROP!');
}
The event
argument is a built-in object, an instance of the Event
object. This event
argument comes with a number of properties and methods, including the dataTransfer
property, which itself is an object.
In other words, we have the following situation (warning: pseudo-code ahead!):
event: {
…,
dataTransfer: {…},
stopPropagation: function(){…},
preventDefault: function(){…},
…,
…,
}
The important thing to conclude from the above structure is that the event
object is just a JS object holding other values, including nested objects and methods. The dataTransfer
object is just one such nested object, which comes with its own set of properties/methods.
In our case, we’re interested in the setData()
and getData()
methods on the dataTransfer
object.
The setData()
and getData()
Methods on The dataTransfer
Object
To be able to successfully “copy” the source element onto the target element, we have to perform a few steps:
- We need to hook into an event handler for an appropriate drag-and-drop related event, as it emits from the source object;
- Once we’re hooked into that specific event (i.e. once we’ve completed the step above), we’ll need to use the
event.dataTransfer.setData()
method to pass in the relevant HTML—which is, of course, the source element; - We’ll then hook into an event handler for another drag-and-drop event—this time, an event that’s emitting from the target object;
- Once we’re hooked into the event handler in the previous step, we’ll need to get the data from step two, using the following method:
event.dataTransfer.getData()
.
That’s all there is to it! To reiterate, we’ll:
- Hook into the
dragstart
event and use theevent.dataTransfer.setData()
to pass in the source element to thedataTransfer
object; - Hook into the
drop
event and use theevent.dataTransfer.getData()
to get the source element’s data from thedataTransfer
object.
So, let’s hook into the dragstart
event handler and get the source element’s data:
sourceElem.addEventListener('dragstart', function(event) {
event.currentTarget.style="opacity:0.3";
targetElem.style = "border: 10px dashed gray;";
event.dataTransfer.setData('text', event.target.id);
})
Next, let’s pass this data on to the drop
event handler:
targetElem.addEventListener('drop', function(event) {
console.log('DROP!');
event.target.appendChild(document.getElementById(event.dataTransfer.getData('text')));
})
We could rewrite this as:
targetElem.addEventListener('drop', function(event) {
console.log('DROP!');
const sourceElemData = event.dataTransfer.getData('text');
const sourceElemId = document.getElementById(sourceElemData);
event.target.appendChild(sourceElemId);
})
Regardless of how we decide to do this, we’ve completed a simple drag-and-drop operation, from start to finish.
Next, let’s use the Drag and Drop API to build a game.
Write a Simple Game Using the Drag and Drop API
In this section, we’ll build a very, very, simple game. We’ll have a row of spans with jumbled lyrics to a famous song.
The user can now drag and drop the words from the first row onto the empty slots in the second row, as shown below.
The goal of the game is to place the lyrics in the correct order.
Let’s begin by adding some HTML structure and CSS styles to our game.
Adding The game’s HTML and CSS
<h1>Famous lyrics game: Abba</h1>
<h2>Instruction: Drag the lyrics in the right order.</h2>
<div id="jumbledWordsWrapper">
<span id="again" data-source-id="again" draggable="true">again</span>
<span id="go" data-source-id="go" draggable="true">go</span>
<span id="I" data-source-id="I" draggable="true">I</span>
<span id="here" data-source-id="here" draggable="true">here</span>
<span id="mia" data-source-id="mia" draggable="true">mia</span>
<span id="Mamma" data-source-id="Mamma" draggable="true">Mamma</span
</div>
<div id="orderedWordsWrapper">
<span data-target-id="Mamma"></span>
<span data-target-id="mia"></span>
<span data-target-id="here"></span>
<span data-target-id="I"></span>
<span data-target-id="go"></span>
<span data-target-id="again"></span>
</div>
The structure is straightforward. We have the static h1
and h2
tags. Then, we have the two divs:
-
jumbledWordsWrapper
, and orderedWordsWrapper
Each of these wrappers holds a number of span tags: one span tag for each word. The span tags in the orderedWordsWrapper
don’t have any text inside, they’re empty.
We’ll use CSS to style our game, as follows:
body {
padding: 40px;
}
h2 {
margin-bottom: 50px;
}
#jumbledWordsWrapper span {
background: wheat;
box-sizing: border-box;
display: inline-block;
width: 100px;
height: 50px;
padding: 15px 25px;
margin: 0 10px;
text-align: center;
border-radius: 5px;
cursor: pointer;
}
#orderedWordsWrapper span {
background: #abcdef;
box-sizing: border-box;
text-align: center;
margin-top: 50px;
}
Next, we’ll add some behavior to our game using JavaScript.
Adding Our Game’s JavaScript Code
We’ll start our JS code by setting a couple of variables and logging them out to make sure we’ve got proper collections:
const jumbledWords = document.querySelectorAll('#jumbledWordsWrapper > span');
const orderedWords = document.querySelectorAll('#orderedWordsWrapper > span');
console.log('jumbledWords: ', jumbledWords);
console.log('orderedWords: ', orderedWords);
The output in the console is as follows:
"jumbledWords: " // [object NodeList] (6)
["<span/>","<span/>","<span/>","<span/>","<span/>","<span/>"]
"orderedWords: " // [object NodeList] (6)
["<span/>","<span/>","<span/>","<span/>","<span/>","<span/>"]
Now that we’re sure that we’re capturing the correct collections, let’s add an event listener on each of the members in the two collections.
On all the source elements, we’ll add methods to handle the dragstart
event firing:
jumbledWords.forEach(el => {
el.addEventListener('dragstart', dragStartHandler);
})
function dragStartHandler(e) {
console.log('dragStartHandler running');
e.dataTransfer.setData('text', e.target.getAttribute('data-source-id'));
console.log(e.target);
}
On all the target elements, we’ll add methods to handle all the relevant drag-and-drop events, namely:
dragenter
dragover
dragleave
drop
The code that follows should be already familiar:
orderedWords.forEach(el => {
el.addEventListener('dragenter', dragEnterHandler);
el.addEventListener('dragover', dragOverHandler);
el.addEventListener('dragleave', dragLeaveHandler);
el.addEventListener('drop', dropHandler);
})
function dragEnterHandler(e) {
console.log('dragEnterHandler running');
}
function dragOverHandler(e) {
console.log('dragOverHandler running');
event.preventDefault();
}
function dragLeaveHandler(e) {
console.log('dragLeaveHandler running');
}
function dropHandler(e) {
e.preventDefault();
console.log('dropHandler running');
const dataSourceId = e.dataTransfer.getData('text');
const dataTargetId = e.target.getAttribute('data-target-id');
console.warn(dataSourceId, dataTargetId);
if(dataSourceId === dataTargetId) {
console.log(document.querySelector([dataTargetId]));
e.target.insertAdjacentHTML('afterbegin', dataSourceId);
}
}
In the dropHandler()
method, we’re preventing the default way that the browser handles the data that comes in. Next, we’re getting the dragged element’s data and we’re saving it in dataSourceId
, which will be the first part of our matching check. Next, we get the dataTargetId
so that we can compare if it is equal to the dataSourceId
.
If dataSouceId
and dataTargetId
are equal, that means that our custom data attributes hold matching values, and thus we can complete the adding of the specific source element’s data into the specific target element’s HTML.
Adding CSS Code For Better UX
Let’s start by inspecting the complete JS code, made slimmer by removal of all redundant console.log()
calls.
const jumbledWords = document.querySelectorAll('#jumbledWordsWrapper > span');
const orderedWords = document.querySelectorAll('#orderedWordsWrapper > span');
jumbledWords.forEach(el => {
el.addEventListener('dragstart', dragStartHandler);
})
orderedWords.forEach(el => {
el.addEventListener('dragenter', dragEnterHandler);
el.addEventListener('dragover', dragOverHandler);
el.addEventListener('dragleave', dragLeaveHandler);
el.addEventListener('drop', dropHandler);
})
function dragStartHandler(e) {
e.dataTransfer.setData('text', e.target.getAttribute('data-source-id'));
}
function dragEnterHandler(e) {
}
function dragOverHandler(e) {
event.preventDefault();
}
function dragLeaveHandler(e) {
}
function dropHandler(e) {
e.preventDefault();
const dataSourceId = e.dataTransfer.getData('text');
const dataTargetId = e.target.getAttribute('data-target-id');
if(dataSourceId === dataTargetId) {
e.target.insertAdjacentHTML('afterbegin', dataSourceId);
}
}
As you can verify above, we’ve removed all the console.log()
invocations, and thus some of our event handler functions are now empty.
That means these functions are ready to receive corresponding CSS code updates. Additionally, due to the updates in style to the dragStartHandler()
method, we’ll also need to add a brand new source element’s event listener for the dragend
event.
We’ll start by adding another event listener to the jumbledWords
collection:
jumbledWords.forEach(el => {
el.addEventListener('dragstart', dragStartHandler);
el.addEventListener('dragend', dragEndHandler);
})
And we’ll update the two event handler function definitions too:
function dragStartHandler(e) {
e.dataTransfer.setData('text', e.target.getAttribute('data-source-id'));
e.target.style = 'opacity: 0.3';
}
function dragEndHandler(e) {
e.target.style = 'opacity: 1';
}
Next, we’ll update the styles inside the dragEnterhandler()
and the dragLeaveHandler()
methods.
function dragEnterHandler(e) {
e.target.style = 'border: 2px dashed gray; box-sizing: border-box; background: whitesmoke';
}
function dragLeaveHandler(e) {
e.target.style = 'border: none; background: #abcdef';
}
We’ll also go around some styling issues by updating the if condition inside the dropHandler()
method:
if(dataSourceId === dataTargetId) {
e.target.insertAdjacentHTML('afterbegin', dataSourceId);
e.target.style = 'border: none; background: #abcdef';
e.target.setAttribute('draggable', false);
}
Preventing Errors
We’ve set up our JS code so that it checks if the values are matching for the data-source-id
of the jumbledWordsWrapper
div and the data-target-id
of the orderedWordsWrapper
div.
This check in itself prevents us from dragging any other word onto the correct place—except for the matching word.
However, we have a bug: there’s no code preventing us from dragging the correct word into the same span inside the orderedWordsWrapper
multiple times.
Here’s an example of this bug:
Obviously, this is a bug that we need to fix. Luckily, the solution is easy: we’ll just get the source element’s data-source-id
, and we’ll use it to build a string which we’ll then use to run the querySelector
on the entire document. This will allow us to find the one source span tag whose text node we used to pass it to the correct target slot. Once we do that, all we need to do is set the draggable
attribute to false
(on the source span element), thus preventing the already used source span element from being dragged again.
if(dataSourceId === dataTargetId) {
e.target.insertAdjacentHTML('afterbegin', dataSourceId);
e.target.style = 'border: none; background: #abcdef';
let sourceElemDataId = 'span[data-source-id="' + dataSourceId + '"]';
let sourceElemSpanTag = document.querySelector(sourceElemDataId);
Additionally, we can give our source element the styling to indicate that it’s no longer draggable. A nice way to do it is to add another attribute: a class
attribute. We can do this with setAttribute
syntax, but here’s another approach, using Object.assign()
:
Object.assign(sourceElemSpanTag, {
className: 'no-longer-draggable',
});
The above syntax allows us to set several attributes, and thus we can also set draggable
to false
as the second entry:
Object.assign(sourceElemSpanTag, {
className: 'no-longer-draggable',
draggable: false,
});
Of course, we also need to update the CSS with the no-longer-draggable
class:
.no-longer-draggable {
cursor: not-allowed !important;
background: lightgray !important;
opacity: 0.5 !important;
}
We have an additional small bug to fix: earlier in the tutorial, we’ve defined the dragEnterHandler()
and the dragLeaveHandler()
functions. The former function sets the styles on the dragged-over target to a dotted border and a pale background, which signals a possible drop location. The latter function reverts these styles to border: none; background: #abcdef
. However, our bug occurs if we drag and try to drop a word into the wrong place. This happens because the dragEnterHandler()
event handler gets called as we drag over the wrong word, but since we never trigger the dragLeaveHandler()
—instead, we triggered the dropHandler()
function—the styles never get reverted.
The solution for this is really easy: we’ll just run the dragLeaveHandler()
at the top of the dropHandler()
function definition, right after the e.preventDefault()
, like this:
function dropHandler(e) {
e.preventDefault();
dragLeaveHandler(e);
Now our simple game is complete!
Here’s the full, completed JavaScript code:
const jumbledWords = document.querySelectorAll('#jumbledWordsWrapper > span');
const orderedWords = document.querySelectorAll('#orderedWordsWrapper > span');
jumbledWords.forEach(el => {
el.addEventListener('dragstart', dragStartHandler);
el.addEventListener('dragend', dragEndHandler);
})
orderedWords.forEach(el => {
el.addEventListener('dragenter', dragEnterHandler);
el.addEventListener('dragover', dragOverHandler);
el.addEventListener('dragleave', dragLeaveHandler);
el.addEventListener('drop', dropHandler);
})
function dragStartHandler(e) {
e.dataTransfer.setData('text', e.target.getAttribute('data-source-id'));
e.target.style = 'opacity: 0.3';
}
function dragEndHandler(e) {
e.target.style = 'opacity: 1';
}
function dragEnterHandler(e) {
e.target.style = 'border: 2px dashed gray; box-sizing: border-box; background: whitesmoke';
}
function dragOverHandler(e) {
event.preventDefault();
}
function dragLeaveHandler(e) {
e.target.style = 'border: none; background: #abcdef';
}
function dropHandler(e) {
e.preventDefault();
dragLeaveHandler(e);
const dataSourceId = e.dataTransfer.getData('text');
const dataTargetId = e.target.getAttribute('data-target-id');
if(dataSourceId === dataTargetId) {
e.target.insertAdjacentHTML('afterbegin', dataSourceId);
e.target.style = 'border: none; background: #abcdef';
let sourceElemDataId = 'span[data-source-id="' + dataSourceId + '"]';
let sourceElemSpanTag = document.querySelector(sourceElemDataId);
Object.assign(sourceElemSpanTag, {
className: 'no-longer-draggable',
draggable: false,
});
}
}
Final Thoughts
Even though our game is finished, this doesn’t have to be the end of the road!
It’s always possible to further improve our code. There are many additional things that can be done here.
We could, for example:
- Add a start and end screen;
- Add a counter that would count the number of attempts;
- Add a countdown timer that would not limit the number of attempts, but rather the time available for us to complete our puzzle game;
- Add more questions;
- Add a leaderboard (we’d need to persist our data somehow);
- Refactor the logic of our game so that we can keep the questions and the order of words in a simple JS object;
- Fetch the questions from a remote API.
There are always more things to learn and more ways to expand our apps. However, all of the above-listed improvements are out of the scope of this tutorial—learning the basics of the Drag and Drop API by building a simple game in vanilla JS.
The fun of coding is in the fact that you can try things on your own. So, try to implement some of these improvements, and make this game your own.
Lastly, if you want to secure your JavaScript source code against theft and reverse-engineering, you can try Jscrambler for free.
Originally published on the Jscrambler Blog by Ajdin Imsirovic.
This content originally appeared on DEV Community and was authored by Jscrambler
Jscrambler | Sciencx (2021-09-21T16:54:14+00:00) Build a Simple Game in Vanilla JS With the Drag and Drop API. Retrieved from https://www.scien.cx/2021/09/21/build-a-simple-game-in-vanilla-js-with-the-drag-and-drop-api/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.