Building Battleship in CSS

This is an experiment to see how far into an interactive experience I can get using only CSS. What better project to attempt than a game? Battleship seemed like a good challenge and a step up from the CSS games I’ve seen so far because it has the complexity of multiple areas that have to interact with two players.

Wanna see the complete game?

View Repo View Demo

Oh, you wanna learn how it works? Let’s dig in.

I could tell right away there was going to be a lot of repetitive HTML and very long CSS selectors coming, so I set up Pug to compile HTML and Less to compile CSS. This is what all the code from here on is going to be written in.

Interactive elements in CSS

In order to get the game mechanics working, we need some interactive elements. We’re going to walk through each one.

HTML checkboxes and :checked

Battleship involves a lot of checking to see whether a field contains a ship or not, so we’re going to use a boatload of checkboxes.

[type*='checkbox'] { // inactive style &:checked { // active style }
}

To style checkboxes, we would first need to reset them using appearance: none; which is only poorly supported right now and needs browser prefixes. The better solution here is to add helper elements. <input> tags can’t have children, including pseudo elements (even though Chrome will render them anyway), so we need to work around that using the adjacent sibling selector.

[type*='checkbox'] { position: relative; opacity: none; + .check-helper { position: absolute; top: 0; left: 0; pointer-events: none; // further inactive styles } &:checked { + .check-helper { // active styles } }
}

If you use a <label> for the helper element, you will also extend the click area of the checkbox onto the helper, allowing you to position it more freely. Also, you can use multiple labels for the same checkbox. Multiple checkboxes for the same label are not supported, however, since you would have to assign the same ID for each checkbox.

Targets

We’re making a local multiplayer game, so we need to hide one player’s battlefield from the other and we need a pause mode allowing for a player to switch without glancing at the other player’s ships. A start screen explaining the rules would also be nice.

HTML already gives us the option to link to a given ID in the document. Using :target we can select the element that we just jumped to. That allows us to create an Single Page Application-like behavior in a completely static document (and without even breaking the back button).

- var screens = ['screen1', 'screen2', 'screen3'];
body nav each screen in screens a(href='#' + screen) each screen in screens .screen(id=screen) p #{screen}
.screen { display: none; &:target { display: block; }
}

Visibility and pointer events

Rendering elements inactive is usually done by using pointer-events: none; The cool thing about pointer-events is that you can reverse it on the child elements. That will leave only the selected child clickable, but the parent stays click-through. This will come in handy later in combination with checkbox helper elements.

The same goes for visibility: hidden; While display: none; and opacity: 0; make the element and all it’s children disappear, visibility can be reversed.

Note that a hidden visibility also disables any pointer events, unlike opacity: 0;, but stays in the document flow, unlike display: none;.

.foo { display: none; // invisible and unclickable .bar { display: block; // invisible and unclickable }
} .foo { visibility: hidden; // invisible and unclickable .bar { visibility: visible; // visible and clickable }
} .foo { opacity: 0; pointer-evens: none; // invisible and unclickable .bar { opacity: 1; pointer-events: all; // still invisible, but clickable }
}
CSS Rule Reversible opacity Reversible pointer events
display: none;
visibility: hidden;
opacity: 0;
pointer-events: none;

OK, now that we’ve established the strategy for our interactive elements, let’s turn to the setup of the game itself.

Setting up

We have some global static variables and the size of our battlefields to define before we actually start:

@gridSize: 12;
@zSea: 1;
@zShips: 1000;
@zAbove: 2000;
@seaColor: #123;
@enemyColor: #f0a;
@playerColor: #0c8;
@hitColor: #f27; body { --grid-measurements: 70vw; @media (min-aspect-ratio: 1/2) { --grid-measurements: 35vh; }
}

The grid size is the size of the battlefield: 12×12 fields in this case. Next, we define some z-indexes and colors.

Here’s the Pug skeleton:

doctype html head title Ships! link(rel="stylesheet", href="style.css") meta(charset="UTF-8") meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no") meta(name="theme-color" content="#000000") body

Everything HTML from this point on will be in the body.

Implementing the states

We need to build the states for Player 1, Player 2, pause, and a start screen. We’ll do this like it was explained above with target selectors. Here’s a little sketch of what we’re trying to achieve:

We have a few modes, each in its own container with an ID. Only one mode is to be displayed in the viewport—the others are hidden via display: none;, except for player modes. If one player is active, the other needs to be outside of the viewport, but still have pointer events so the players can interact with each other.

.mode#pause each party in ['p1', 'p2'] .mode(id=party) .mode#start .status each party in ['p1', 'p2'] a.player-link.switch(href='#' + party) a.status-link.playpause(href='#pause') End Turn h1 Ships!

The .status div contains the main navigation. Its entries will change depending on the active mode, so in order to select it properly, we’ll need put it after our .mode elements. The same goes for the <h1>, so it ends up at the end of the document (don’t tell the SEO people).

.mode { opacity: 0; pointer-events: none; &:target, &#start { opacity: 1; pointer-events: all; z-index: 1; } &#p1, &#p2 { position: absolute; transform: translateX(0); opacity: 1; z-index: 2; } &#p1:target { transform: translateX(50vw); +#p2 { transform: translateX(50vw); z-index: 2; } } &#p2 { transform: translateX(50vw); z-index: 1; } &#pause:target { ~ #p1, ~ #p2 { opacity: 0; } }
} #start { .mode:target ~ & { display: none; }
}

The .mode div never has pointer events and always is fully transparent (read: inactive), except for the start screen, which is enabled by default and the currently targeted screen. I don’t simply set it to display: none; because I still need it to be in the document flow. Hiding the visibility won’t work because I need to activate pointer events individually later on, when hitting enemy ships.

I need #p1 and #p2 to be next to each other because that’s what’s going to enable the interaction between one players hits and the other players ships.

Implementing the battlefields

We need two sets of two battlefields for a total of four battlefields. Each set contains one battlefield for the current player and another for the opposite player. One set is going to be in #p1 and the other one in #p2. Only one of the players will be in the viewport, but both retain their pointer events and their flow in the document. Here’s a little sketch:

Now we need lots of HTML. Each player needs two battlefields, which need to have 12×12 fields. That’s 576 fields in total, so we’re going to loop around a bit.

The fields are going to have their own class declaring their position in the grid. Also, fields in the first row or line get a position indicator, so you get to say something cool like “Fire at C6.”

each party in 'p1', 'p2'] .mode(id=party) each faction in 'enemy', 'player'] .battlefield(class=faction, class=party) each line in 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] each col, colI in 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'] div(class='field-x' + (colI+1) + '-y' + line) if (col === 'A') .indicator-col #{line} if (line === 1) .indicator-line #{col}

The battlefield itself is going to be set in a CSS grid, with its template and measurements coming from the variables we set before. We’ll position them absolutely within our .mode divs and switch the enemy position with the player. In the actual board game, you have your own ships on the bottom as well. Note that we need to escape the calc on the top value, or Less will try to calculate it for you and fail.

.battlefield { position: absolute; display: grid; grid-template-columns: repeat(@gridSize, 1fr); width: var(--grid-measurements); height: var(--grid-measurements); margin: 0 auto 5vw; border: 2px solid; transform: translate(-50%, 0); z-index: @zSea; &.player { top: calc(var(--grid-measurements) ~"+" 150px); border-color: transparent; :target & { border-color: @playerColor; } } &.enemy { top: 100px; border-color: transparent; :target & { border-color: @enemyColor; } }
}

We want the tiles of the battlefield to be a nice checkerboard pattern. I wrote a mixin to calculate the colors, and since I like my mixins separated from the rest, this is going into a components.less file.

.checkerboard(@counter) when (@counter > 0) { .checkerboard(@counter - 2); &[class^='field-'][class$='-y@{counter}'] { &:nth-of-type(odd) { background-color: transparent; :target & { background-color: darken(@seaColor, 3%); } } &:nth-of-type(even) { background-color: transparent; :target & { background-color: darken(@seaColor, 4%); } } }
}

When we call it with .checkerboard(@gridSize);, it will iterate through every second line of the grid and set background colors for odd and even instances of the current element. We can color the remaining fields with an ordinary :odd and :even.

Next, we place the indicators outside of the battlefields.

[class^='field-'] { position: relative; display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; background-color: transparent; .checkerboard(@gridSize); :target &:nth-of-type(even) { background-color: darken(@seaColor, 2%); } :target &:nth-of-type(odd) { background-color: darken(@seaColor, 1%); } [class^='indicator-'] { display: none; :target & { position: absolute; display: flex; justify-content: center; width: calc(var(--grid-measurements)~"/"@gridSize); height: calc(var(--grid-measurements)~"/"@gridSize); color: lighten(@seaColor, 10%); pointer-events: none; } &.indicator-line { top: -1.5em; align-items: flex-start; } &.indicator-col { left: -2.3em; align-items: center; } }
}

Implementing the ships

Let’s get to the tricky part and place some ships. Those need to be clickable and interactive, so they’re going to be checkboxes. Actually, we need two checkboxes for one ship: miss and hit.

  • Miss is the bottom one. If nothing else is on that field, your shot hits the water and triggers a miss-animation. The exception is when a player clicks on their own battlefield. In that case, the ship animation plays.
  • When an own ships spawns, it activates a new checkbox. This one is called hit. It’s placed at the exact same coordinates as its corresponding ship, but in the other players attack field and above the checkbox helper for the miss. If a hit is activated, it displays a hit animation on the current player’s attack field as well as well as on the opponent’s own ship.

This is why we need to position our battlefields absolutely next to each other. We need them aligned at all times in order to let them interact with each other.

First, we’re going to set some styles that apply to both checkboxes. We still need the pointer events, but want to visually hide the checkbox and work with helper elements instead.

.check { position: absolute; top: 0; left: 0; width: 100%; height: 100%; margin: 0; opacity: 0; + .check-helper { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
}

We’ll also write some classes for our events for later use right now. This will also go into components.less:

.hit-obj { position: absolute; visibility: visible; left: 0; top: 0; width: 100%; height: 100%; border-radius: 50%; animation: hit 1s forwards;
} .ship-obj { position: absolute; left: 0; top: 0; width: 90%; height: 90%; border-radius: 15%; animation: setShip 0.5s forwards;
} .miss-obj { position: absolute; left: 0; top: 0; width: 100%; height: 100%; border-radius: 50%; animation: miss 1s forwards;
}

Spawning and missing ships

Those two events are basically the same. If you hit the sea in your own battlefield, you create a ship. If you hit the sea in the enemy battlefield, you trigger a miss. That happens by calling the respective class from our components.less file within a pseudo element of the helper class. We use pseudo elements here because we need to place two objects in one helper later on.

If you spawn a ship, you shouldn’t be able to un-spawn it, so we make it lose its pointer events after being checked. However, the next hit-checkbox gains it pointer events, enabling the enemy to hit spawned ships.

.check { &.ship { &:checked { pointer-events: none; } &:checked + .check-helper { :target .player & { &::after { content: ""; .ship-obj; // set own ship } } :target .enemy & { &::after { content: ""; .miss-obj; // miss enemy ship } } } &:checked ~ .hit { pointer-events: all; } }
}

Hitting ships

That new hit checkbox is positioned absolutely on top of the other player’s attack field. For Player 1 that means by 50vw to the right and by the grid height + 50px margin to the top. It has no pointer events by default, they are going to be overwritten by those set in .ship:check ~ .hit, so only ships that are actually set, can be hit.

To display a hit event, we need two pseudo elements: one that confirms the hit on the attack field; and one that shows the victim where they have been hit. :checked + .check-helper::after calls a .hit-obj from components.less onto the attacker’s field and the corresponding ::before pseudo element gets translated back to the victim’s own battlefield.

Since the display of hit events isn’t scoped to the active player, we need to remove all unnecessary instances manually using display: none;.

.check { &.hit { position: absolute; top: ~"calc(-1 * (var(--grid-measurements) + 50px))"; left: 50vw; width: 100%; height: 100%; pointer-events: none; #p2 &, #p1:target & { left: 0; } #p1:not(:target) & + .check-helper::before { left: 50vw; } &:checked { opacity: 1; visibility: hidden; pointer-events: none; + .check-helper { &::before { content: ""; .hit-obj; // hit enemy ships top: ~"calc(-1 * (var(--grid-measurements) + 50px))"; } &::after { content: ""; .hit-obj; // hit own ships top: -2px; left: -2px; } #p1:target &::before, #p1:target ~ #p2 &::after, #p1:not(:target) &::after, #p2:target &::before { display: none; } } } #p1:target .battlefield.p1 &, #p2:target .battlefield.p2 & { display: none; } }
}

Animating the events

While we did style our miss, ship and hit objects, there’s nothing to be seen yet. That’s because we are still missing the animations making those objects visible. Those are simple keyframe animations that I put into a new Less file called animations.less.

@keyframes setShip { 0% { transform: scale(0, 0); background-color: transparent; } 100% { transform: scale(1, 1); background-color: @playerColor; }
} @keyframes hit { 0% { transform: scale(0, 0); opacity: 0; background-color: transparent; } 10% { transform: scale(1.2, 1.2); opacity: 1; background-color: spin(@hitColor, 40); box-shadow: 0 0 0 0.5em var(--shadowColor); } 100% { transform: scale(.7, .7); opacity: .7; background-color: @hitColor; box-shadow: 0 0 0 0.5em var(--shadowColor); }
} @keyframes miss { 0% { transform: scale(0, 0); opacity: 1; background-color: lighten(@seaColor, 50); } 100% { transform: scale(1, 1); opacity: .8; background-color: lighten(@seaColor, 10); }
}

Add customizable player names

This isn’t really necessary for functionality, but it’s a nice little extra. Instead of being called “Player 1” and “Player 2,” you can enter your own name. We do this by adding two <input type="text"> to .status, one for each player. They have placeholders in case the players don’t want to enter their names and want to skip to the game right away.

.status input(type="text" placeholder="1st Player").player-name#name1 input(type="text" placeholder="2nd Player").player-name#name2 each party in ['p1', 'p2'] a.player-link.switch(href='#' + party) a.status-link.playpause(href='#pause') End Turn

Because we put them into .status, we can display them on every screen. On the start screen, we leave them as normal input fields, for the players to enter their names. We style their placeholders to look like the actual text input, so it doesn’t really matter if players enter their names or not.

.status { .player-name { position: relative; padding: 3px; border: 1px solid @enemyColor; background: transparent; color: @playerColor; &::placeholder { color: @playerColor; opacity: 1; // Reset Firefox user agent styles } }
}

On the other screens, we remove their typical input field styles as well as their pointer events, making they appear as normal, non-changeable text. .status also contains empty links to select players. We style those links to have actual measurements and display the name inputs without pointer events above them. Clicking a name triggers the link now, targeting the corresponding mode.

.status { .mode#pause:target ~ & { top: 40vh; width: calc(100% ~"-" 40px); padding: 0 20px; text-align: center; z-index: @zAbove; .player-name, .player-link { position: absolute; display: block; width: 80%; max-width: 500px; height: 40px; margin: 0; padding: 0; &:nth-of-type(even) { top: 60px; } } .player-name { border: 0; text-align: center; pointer-events: none; } }
}

The player screens only need to display the active player, so we remove the other one.

.status { .mode#p1:target ~ & #name2 { display: none; } .mode#p2:target ~ & #name1 { display: none; }
}

Some notes on the Internet Explorer and Edge: Microsoft browsers haven’t implemented the ::placeholder pseudo element. While they do support :-ms-input-placeholder for IE and ::-ms-input-placeholder, as well as the webkit-prefix for Edge, those prefixes only work if ::placeholder is not set. As far as I played around with placeholders, I only managed to style them properly in either the Microsoft browsers, or all the other ones. If someone else has a workaround, please share it!

Putting it all together

What we have so far is a functional, but not very handsome game. I use the start screen to clarify some basic rules. Since we don’t have a hard-coded win condition and nothing to prevent players to place their ships wildly all over the place, I created a “Play fair” note that encourages the good ol’ honor system.

.mode#start .battlefield.enemy ol li span You are this color. li span Your enemy is span this span color li span You may place your ships as follows: ul li 1 x 5 blocks li 2 x 4 blocks li 3 x 3 blocks li 4 x 2 blocks

I’m not going into the detail of how I went about getting things exactly to my liking since most of that is very basic CSS. You can go through the end result to pick them out.

When we finally connect all the pieces, we get this:

See the Pen CSS Game: Battleships by Daniel Schulz (@iamschulz) on CodePen.

Wrapping up

Let’s look back at what we’ve accomplished.

HTML and CSS may not be programming languages, but they are mighty tools in their own domain. We can manage states with pseudo classes and manipulate the DOM with pseudo elements.

While most of us use :hover and :focus all the time, :checked goes by largely unnoticed, only for styling actual checkboxes and radio buttons at best. Checkboxes are handy little tools that can help us to get rid of unnecessary JavaScript in our more simple front end features. I wouldn’t hesitate to build dropdown or off-canvas menus in pure CSS in real life projects, as long as the requirements don’t get too complicated.

I’d be a bit more cautious when using the :target selector. Since it uses the URL hash value, it’s only usable for a global value. I think I’d use it for, say, highlighting the current paragraph on a content page, but not for reusable elements like a slider or an accordion menu. It can also quickly get messy on larger projects, especially when other parts of it start controlling the hash value.

Building the game was a learning experience for me, dealing with pseudo selectors interacting with each other and playing around with lots of pointer events. If I had to build it again, I’d surely choose another path, which is a good outcome for me. I definitely don’t see it as a production-ready or even clean solution, and those super specific selectors are a nightmare to maintain, but it has some good parts in it that I can transition to real life projects.

Most importantly though, it was a fun thing to do.

The post Building Battleship in CSS appeared first on CSS-Tricks.

Creating a Parking Game With the HTML Drag and Drop API

Among the many JavaScript APIs added in HTML5 was Drag and Drop (we’ll refer to it as DnD in this article) which brought native DnD support to the browser, making it easier for developers to implement this interactive feature into applications. The amazing thing that happens when features become easier to implement is that people start making all kinds of silly, impractical things with it, like the one we’re making today: a parking game!

DnD requires only a few things to work:

  • Something to drag
  • Somewhere to drop
  • JavaScript event handlers on the target to tell the browser it can drop

We’re going to start by creating our draggables.

Dragging

Both <img> and <a>(with the href attribute set) elements are draggable by default. If you want to drag a different element, you’ll need to set the draggable attribute to true.

We’ll start with the HTML that sets up the images for our four vehicles: fire truck, ambulance, car and bicycle.

<ul class="vehicles"> <li> <!-- Fire Truck --> <!-- <code>img<code> elements don't need a <code>draggable<code> attribute like other elements --> <img id="fire-truck" alt="fire truck" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Ftruck-clip-art-fire-truck4.png?1519011787956"/> </li> <li> <!-- Ambulance --> <img id="ambulance" alt="ambulance" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Fambulance5.png?1519011787610"> </li> <li> <!-- Car --> <img id="car" alt="car" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Fcar-20clip-20art-1311497037_Vector_Clipart.png?1519011788408"> </li> <li> <!-- Bike --> <img id="bike" alt="bicycle" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Fbicycle-20clip-20art-bicycle3.png?1519011787816"> </li>
</ul>

Since images are draggable by default, you’ll see dragging any one of them creates a ghost image.

Just adding a draggable attribute to an element that’s not an image or link is really all you need to make an element draggable in most browsers. To make elements draggable in all browsers, you need to define some event handlers. They are also useful for adding extra functionality like a border if an element is being dragged around or a sound if it stops being dragged. For these, you’re going to need some drag event handlers, so let’s look at those.

Drag Events

There are three drag-related events you can listen for but we’re only going to use two: dragstart and dragend.

  • dragstart – Triggered as soon as we start dragging. This is where we can define the drag data and the drag effect.
  • dragend – Triggered when a draggable element is dropped. This event is generally fired right after the drop zone’s drop event.

We’ll cover what the drag data and the drag effect is shortly.

let dragged; // Keeps track of what's being dragged - we'll use this later! function onDragStart(event) { let target = event.target; if (target && target.nodeName === 'IMG') { // If target is an image dragged = target; event.dataTransfer.setData('text', target.id); event.dataTransfer.dropEffect = 'move'; // Make it half transparent when it's being dragged event.target.style.opacity = .3; }
} function onDragEnd(event) { if (event.target && event.target.nodeName === 'IMG') { // Reset the transparency event.target.style.opacity = ''; // Reset opacity when dragging ends dragged = null; }
} // Adding event listeners
const vehicles = document.querySelector('.vehicles');
vehicles.addEventListener('dragstart', onDragStart);
vehicles.addEventListener('dragend', onDragEnd);

There are a couple of things happening in this code:

  • We are defining the drag data. Each drag event has a property called dataTransfer that stores the event’s data. You can use the setData(type, data) method to add a dragged item to the drag data. We’re storing the dragged image’s ID as type 'text' in line 7.
  • We’re storing the element being dragged in a global variable. I know, I know. Global is dangerous for scoping but here’s why we do it: although you can store the dragged item using setData, you can’t retrieve it using event.dataTransfer.getData() in all browsers (except Firefox) because the drag data is protected mode. You can read more about it here. I wanted to mention defining the drag data just so you know about it.
  • We’re setting the dropEffect to move. The dropEffect property is used to control the feedback the user is given during a drag and drop operation. For example, it changes which cursor the browser displays while dragging. There are three effects: copy, move and link.
    • copy – Indicates that the data being dragged will be copied from its source to the drop location.
    • move – Indicates that the data being dragged will be moved.
    • link – Indicates that some form of relationship will be created between the source and drop locations.

Now we have draggable vehicles but nowhere to drop them:

See the Pen 1 – Can you park here? by Omayeli Arenyeka (@yelly) on CodePen.

Dropping

By default, when you drag an element, only form elements such as <input> will be able to accept it as a drop. We’re going to contain our “dropzone” in a <section> element, so we need to add drop event handlers so it can accept drops just like a form element.

First, since it’s an empty element we’re going to need to set a width, height and background color on it so we can see it on screen.

These are the parameters we have available for drop events:

  • dragenter – Triggered at the moment a draggable item enters a droppable area. At least 50% of the draggable element has to be inside the drop zone.
  • dragover – The same as dragenter but it is called repeatedly while the draggable item is within the drop zone.
  • dragleave – Triggered once a draggable item has moved away from a drop zone.
  • drop – Triggered when the draggable item has been released and the drop area agrees to accept the drop.
function onDragOver(event) { // Prevent default to allow drop event.preventDefault();
} function onDragLeave(event) { event.target.style.background = '';
} function onDragEnter(event) { const target = event.target; if (target) { event.preventDefault(); // Set the dropEffect to move event.dataTransfer.dropEffect = 'move' target.style.background = '#1f904e'; }
} function onDrop(event) { const target = event.target; if ( target) { target.style.backgroundColor = ''; event.preventDefault(); // Get the id of the target and add the moved element to the target's DOM dragged.parentNode.removeChild(dragged); dragged.style.opacity = ''; target.appendChild(dragged); }
} const dropZone = document.querySelector('.drop-zone');
dropZone.addEventListener('drop', onDrop);
dropZone.addEventListener('dragenter', onDragEnter);
dropZone.addEventListener('dragleave', onDragLeave);
dropZone.addEventListener('dragover', onDragOver);

If you’re wondering why we keep calling event.preventDefault() it’s because by default the browser assumes any target is not a valid drop target. This isn’t true all the time for all browsers but it’s better to be safe than sorry! Calling preventDefault() on the dragenter, dragover and drop events, informs the browser that the current target is a valid drop target.

Now, we have a simple drag and drop application!

See the Pen 2 – Can you park here? by Omayeli Arenyeka (@yelly) on CodePen.

It’s fun, but not quite as frustrating as parking. We have to create some rules to make that happen.

Rules and Validation

I came up with some random parking rules, and I’d encourage you to create some of your own. Parking signs usually have days and times you can park as well as what types of vehicles are allowed to park at that moment in time. When we were creating our draggable objects, we had four vehicles: an ambulance, a fire truck, a regular car and a bicycle. So, we’re going to create rules for them.

  1. Ambulance parking only: Monday through Friday, 9pm to 3am.
  2. Fire truck parking only: All day during the weekend.
  3. Regular car parking: Monday through Friday, 3am to 3pm.
  4. Bicycle parking: Monday through Friday, 3pm to 9pm.

Now, we translate these rules to code. We’re going to be using two libraries to handle time and ranges: Moment and Moment-range.

The scripts are already available in Codepen to add to any new demo, but if you are developing outside of Codepen you can copy or link them up from here:

<script defer src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/moment-range/3.1.1/moment-range.js"></script>

Then, we create an object to store all the parking rules.

window['moment-range'].extendMoment(moment); // The array of weekdays
const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
const parkingRules = { ambulance: { // The ambulance can only park on weekdays... days: weekdays, // ...from 9pm to 3am (the next day) times: createRange(moment().set('hour', 21), moment().add(1, 'day').set('hour', 3)) }, 'fire truck': { // The fire truck can obnly park on Saturdays and Sundays, but all day days: ['Saturday', 'Sunday'] }, car: { // The car can only park on weekdays... days: weekdays, // ...from 3am - 3pm (the same day) times: createRange(moment().set('hour', 3), moment().set('hour', 15)) }, bicycle: { // The car can only park on weekdays... days: weekdays, // ...from 3pm - 9pm (the same day) times: createRange(moment().set('hour', 15), moment().set('hour', 21)) }
}; function createRange(start, end) { if (start && end) { return moment.range(start, end); }
}

Each vehicle in the parkingRules object has a days property with an array of days it can park and a times property that is a time range. To get the current time using Moment, call moment(). To create a range using Moment-range, pass a start and end time to the moment.range function.

Now, in the onDragEnter and onDrop event handlers we defined earlier, we add some checks to make sure a vehicle can park. Our alt attribute on the img tag is storing the type of vehicle so we pass that to a canPark method which will return if the car can be parked. We also added visual cues (change in background) to tell the user whether a vehicle can be parked or not.

function onDragEnter(event) { const target = event.target; if (dragged && target) { const vehicleType = dragged.alt; // e.g bicycle, ambulance if (canPark(vehicleType)) { event.preventDefault(); // Set the dropEffect to move event.dataTransfer.dropEffect = 'move'; /* Change color to green to show it can be dropped /* target.style.background = '#1f904e'; } else { /* Change color to red to show it can't be dropped. Notice we * don't call event.preventDefault() here so the browser won't * allow a drop by default */ target.style.backgroundColor = '#d51c00'; } }
} function onDrop(event) { const target = event.target; if (target) { const data = event.dataTransfer.getData('text'); const dragged = document.getElementById(data); const vehicleType = dragged.alt; target.style.backgroundColor = ''; if (canPark(vehicleType)) { event.preventDefault(); // Get the ID of the target and add the moved element to the target's DOM dragged.style.opacity = ''; target.appendChild(dragged); } }
}

Then, we create the canPark method.

function getDay() { return moment().format('dddd'); // format as 'monday' not 1
} function getHours() { return moment().hour();
} function canPark(vehicle) { /* Check the time and the type of vehicle being dragged * to see if it can park at this time */ if (vehicle && parkingRules[vehicle]) { const rules = parkingRules[vehicle]; const validDays = rules.days; const validTimes = rules.times; const curDay = getDay(); if (validDays) { /* If the current day is included on the parking days for the vehicle * And if the current time is within the range */ return validDays.includes(curDay) && (validTimes ? validTimes.contains(moment()) : true); /* Moment.range has a contains function that checks * to see if your range contains a moment. https://github.com/rotaready/moment-range#contains */ } } return false;
}

Now, only cars that are allowed to park can park. Lastly, we add the rules to the screen and style it.

Here’s the final result:

See the Pen 3 – Can you park here? by Omayeli Arenyeka (@yelly) on CodePen.

There are lots of ways this could be improved:

  • Auto-generate the HTML for the rules list from the parkingRules object!
  • Add some sound effects!
  • Add ability to drag back vehicles to original point without a page refresh.
  • All those pesky global variables.

But I’ll let you handle that.

If you’re interested in learning more about the DnD API and some critiques of it, here’s some good reading:

  • WHATWG Specification
  • Working with HTML5 Drag-and-Drop – Pro HTML5 Programming, Chapter 9, by Jen Simmons
  • Accessible Drag and Drop Using WAI-ARIA – Accessibility considerations from Dev.Opera
  • Native HTML5 Drag and Drop – HTML5 Rocks tutorial
  • The HTML5 drag and drop disaster – QuirksMode post with helpful context on the DnD module implementation

The post Creating a Parking Game With the HTML Drag and Drop API appeared first on CSS-Tricks.