Making the puzzle pieces draggable
Now it's time to kickstart jQuery UI to make the individual pieces of the puzzle draggable.
jQuery UI is a suite of jQuery plugins used to build interactive and efficient user interfaces. It is stable, mature, and is recognized as the official, although not the only UI library for jQuery.
Prepare for Lift Off
In this task we'll cover the following steps:
Making the puzzle pieces draggable using jQuery UI's Draggable component
Configuring the draggables so that only pieces directly next to the empty space can be moved
Configuring the draggables so that pieces can only be moved into the empty space
Engage Thrusters
First we'll make the pieces draggable and set some of the configuration options that the component exposes. This code should be added to sliding-puzzle.js
, directly after the code added in the previous task:
pieces.draggable({ containment: "parent", grid: [pieceW, pieceH], start: function (e, ui) { }, drag: function (e, ui) { }, stop: function (e, ui) { } });
The next few steps in this task will see additional code added to the start
, drag
, and stop
callback functions in the previous code sample.
We also need to configure the draggability so that the pieces can only be moved into the empty space, and not over each other, and so that only pieces directly adjacent to the empty space can be moved at all.
Next add the following code in to the start
callback function that we just added:
var current = getPosition(ui.helper); if (current.left === empty.left) { ui.helper.draggable("option", "axis", "y"); } else if (current.top === empty.top) { ui.helper.draggable("option", "axis", "x"); } else { ui.helper.trigger("mouseup"); return false; } if (current.bottom < empty.top || current.top > empty.bottom || current.left > empty.right || current.right < empty.left) { ui.helper.trigger("mouseup"); return false; } previous.top = current.top; previous.left = current.left;
Next, add the following code to the drag
callback function:
var current = getPosition(ui.helper); ui.helper.draggable("option", "revert", false); if (current.top === empty.top && current.left === empty.left) { ui.helper.trigger("mouseup"); return false; } if (current.top > empty.bottom || current.bottom < empty.top || current.left > empty.right || current.right < empty.left) { ui.helper.trigger("mouseup") .css({ top: previous.top, left: previous.left }); return false; }
Finally, we should add the following code to the stop
callback function:
var current = getPosition(ui.helper); if (current.top === empty.top && current.left === empty.left) { empty.top = previous.top; empty.left = previous.left; empty.bottom = previous.top + pieceH; empty.right = previous.left + pieceW; }
In each of our callbacks we've used a helper function that returns the exact position of the current draggable. We should also add this function after the draggable()
method:
function getPosition(el) { return { top: parseInt(el.css("top")), bottom: parseInt(el.css("top")) + pieceH, left: parseInt(el.css("left")), right: parseInt(el.css("left")) + pieceW } }
Objective Complete - Mini Debriefing
We wrote a lot of code in that last task, so let's break it down and see what we did. We started by making the pieces draggable using the jQuery UI draggable component. We did this by calling the draggable()
method, passing in an object literal that sets various options that the draggable component exposes.
First we set the containment
option to parent
, which stops any of the pieces being dragged out of the <figure>
element that they are within. We also set the grid
option, which allows us to specify a grid of points that the piece being dragged should snap to. We set an array as the value of this option.
The first item in this array sets the horizontal points on the grid and the second item sets the vertical points on the grid. Setting these options gives the movement of the pieces a more realistic and tactile experience.
The next and final three options that we set are actually callback functions that are invoked at different points in the life-cycle of a drag. We use the start
, drag
, and stop
callbacks.
When the drag begins
The start
callback will be invoked once at the very start of the drag interaction following a mousedown
event on a draggable. The stop
callback will be invoked once at the very end of a drag interaction, once a mouseup
event has registered. The drag
callback will fire almost continuously while a piece is being dragged as it is invoked for every pixel the dragged element moves.
Let's look at the start
callback first. Each callback is passed two arguments by jQuery UI when it is invoked. The first of these is the event object, which we don't require in this project, while the second is an object containing useful properties about the current draggable.
At the beginning of the function we first get the exact position of the piece that dragging has started on. When we call our getPosition()
function, we pass in the helper
property of the ui
object, which is a jQuery-wrapped reference to the underlying DOM element that has started to be dragged.
Once we have the element's position, we first check whether the element is in the same row as the empty space by comparing the left
property of the current object (the object returned by getPosition()
) with the left
property of the empty
object.
If the two properties are equal, we set the axis
option of the draggable to y
so that it can only move horizontally. Configuration options can be set in any jQuery UI widget or component using the option
method.
If it isn't in the same row, we check whether it is in the same column instead by comparing the top
properties of the current
and empty
objects. If these two properties are equal, we instead set the axis
option to x
so that the piece may only move vertically.
If neither of these conditions is true, the piece cannot be adjacent to the empty space, so we manually trigger a mouseup
event to stop the drag using jQuery's trigger()
method, and also return false
from the function so that our stop
handler is not triggered.
We need to make sure that only squares in the same row or column as the empty space are draggable, but we also need to make sure that any pieces that are not directly adjacent to the empty space cannot be dragged either.
To stop any pieces not adjacent to the empty space being dragged, we just check that:
The bottom of the current piece is less than the top of the empty space
The top of the current piece is greater than the bottom of the empty space
The left of the current piece is greater than the right of the empty space
The right of the current piece is less than the left of the empty space
If any of these conditions are true, we again stop the drag by triggering a mouseup
event manually, and stop any further event handlers on the draggable being called (but only for the current drag interaction) by returning false
.
If the callback function has not returned at this point, we know we are dealing with a draggable that is adjacent to the empty space, thereby constituting a valid drag object. We therefore store its current position at the start of the drag for later use by setting the top
and left
properties of the previous
object that we initialized at the start of the project.
Tip
The position of ui.helper
The ui
object passed to our callback function actually contains an object called position
, which can be used to obtain the current draggable's position. However, because we are using the grid
option, the values contained in this object may not be granular enough for our needs.
During the drag
Next we can walk through the drag
callback, which will be called every time the position of the current draggable changes. This will occur during a mousedown
event.
First of all we need to know where the piece that's being dragged is, so we call our getPosition()
helper function again.
Then we want to check whether the piece being dragged is in the empty space. If it is, we can stop the drag in the same way that we did before – by manually triggering a mouseup
event and returning false
.
During the drag, only valid pieces will be draggable because we've already filtered out any pieces that are not directly adjacent to the empty space. However, we still need to check that the piece being dragged is not being dragged away from the empty space. We do this in the same way that we filtered out pieces not adjacent to the empty space in the start
callback.
When the drag ends
The stop
callback is the simplest of the three callbacks. We get the position of the piece that was dragged, and if it's definitely in the empty space, we move the empty space so that it is in the position the dragged piece was in when the drag began. Remember we stored this information in an object called previous
.