Mastering jQuery UI
上QQ阅读APP看书,第一时间看更新

Making the puzzle functional

Before writing any JavaScript code to create a functional puzzle, let's write down the features of our puzzle and see how we will achieve them.

When the page loads, an image will be displayed to the user in puzzleContainer, and a Start button will be displayed under it. The image will actually be a collection of 16 different div elements, each having the same background image but a different background position. Using the background-position CSS property, we will be able to display the complete image to the user. Once the Start button is clicked, we will take these 16 images and place them at random positions inside pieceBox. We will also display a 4 x 4 grid, puzzleContainer, where any of the 16 pieces could be dropped. We will then attach appropriate event handlers that will allow us to drag an individual puzzle piece from pieceBox to puzzleContainer. Once a piece has been moved to puzzleContainer, it cannot be dropped back to pieceBox. It can, however, be dragged into any other cell in puzzleContainer. Once the user has arranged all the pieces, a relevant message will be displayed.

Enough with the theory for now! Let's dive into some practical JavaScript. In your text editor, open the puzzle.js file.

Creating slices of the image

Write the following code in the puzzle.js file:

var rows = 4;
var cols = 4;
$(document).ready(function(){
  var sliceStr = createSlices(true);
  $('#puzzleContainer').html(sliceStr);
});

function createSlices(useImage){
  var str = '';
  var sliceArr = [];
  for(var i=0, top=0, c=0; i < rows; i++, top-=100)
  {
    for(var j=0, left=0; j<cols; j++, left-= 100, c++)
    {
      if(useImage)
      {
        sliceArr.push('<div style="background-position: ' + left + 'px ' + top +'px;" class="img" data-sequence="'+c+'">');
      }
      else
      {
        sliceArr.push('<div style="background-image:none;" class="img imgDroppable">');
      }
      sliceArr.push('</div>');
    }
  }
  return sliceArr.join('');
}

The 16 div elements will be in the form of a grid of 4 rows and 4 columns. In the preceding code, we defined two variables, rows and cols, and set their value to 4.

Next, there is the $(document).ready(function() handler, in which we will write our code. Inside this handler, we call the createSlices function. This function will create the required 16 div elements and return a string with their HTML structure. This string will then be inserted into the puzzleContainer div element.

After you have written this code, save the puzzle.js file and refresh the index.html page on your browser. You will see a screen resembling the following screenshot:

Creating slices of the image

Now let's look at the createSlices function in detail.

We defined a variable named str to store the HTML structure. Next, there are two for loops. In the outer loop, we initialized another variable named top to 0, which will be decremented by 100 in each iteration.

Similarly, inside the inner loop, another variable named left is defined, and this will also be decreased by 100 in each iteration. Inside the inner loop, a div element is created, where we set the div's left and top values using the background-position CSS property. This is done in order to create all 16 slides with appropriate images.

A CSS class named img is also added to the div element. We have already defined CSS properties for this class in the index.html file. This class sets the background image as kitty.jpg for the div element. It also defines the height and width of the div as 100 px each, and a border of 1 px is also applied.

A data attribute named data-sequence is also added to each div. This attribute will be used later to check whether all the div elements are arranged correctly or not. Its value will be 0 for the first div, 1 for the second div, 2 for the third div, and so on until 15, which is set as a value for the last div. Once both the loops are completed, we return the complete DOM structure from the function. This structure will now be inserted in div puzzleContainer.

The CSS background-position property

To create a complete image using different pieces, we will need perfect placement of the background-image property. The background-position property defines the starting left and top positions of the background image for that specific div element. Therefore, if we define the background position as background-position: 0px 0px, it means that the image will get positioned at the top-left corner of element. Similarly, if we set background-position: -100px 0px, the left corner will skip the initial 100 pixels of the image.

To understand this more clearly, go to the browser page and inspect the DOM using Firebug (you can download this for Firefox from https://addons.mozilla.org/en-US/firefox/addon/firebug/) or Chrome DevTools (check out the help on Google Chrome DevTools at https://developer.chrome.com/devtools). You will see that the DOM structure resembles the following screenshot:

The CSS background-position property

This structure clearly shows 16 different divs, each having a different background-position setting. You can play with these values in Firebug or Chrome Developer tools in the options provided by the browser by increasing or decreasing their values to see how the background images are positioned on a puzzle piece.

Starting the game

Now that we have our puzzle pieces ready, we need to implement the Start button by adding an event handler for it. This event handler will shuffle all the slices created earlier and will place them at random positions in the div, having pieceBox as the id. The following code needs to be added to the $(document).ready(function() handler:

$('#start').on('click', function()
{
  var divs = $('#puzzleContainer > div');
  var allDivs = shuffle(divs);
  $('#pieceBox').empty();
  allDivs.each(function(){
    var leftDistance = Math.floor((Math.random()*280)) + 'px';
    var topDistance = Math.floor((Math.random()*280)) + 'px';
    $(this)
    .addClass('imgDraggable')
    .css({
      position : 'absolute',
      left : leftDistance,
      top : topDistance
    });
    $('#pieceBox').append($(this));
  });

  var sliceStr = createSlices(false);
  $('#puzzleContainer').html(sliceStr);

  $(this).hide();
  $('#reset').show();

});

Also, outside the $(document).ready(function() handler, define the shuffle function. This is the same function that we used in Chapter 1, Designing a Simple Quiz Application:

function shuffle(o)
{
  for(var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
  return o;
}

We registered a click event handler to the list item with the start ID. In the first line, we find all the div elements inside puzzleContainer in a divs variable. We pass this array to the shuffle function in the next line, which randomizes this array and returns it in a variable named allDivs. Now the allDivs variable is an array of puzzle pieces (div elements) in a random order. We need to place these pieces in the piecebox div.

Since we want these pieces to look scattered inside the pieceBox div, first we loop over the elements of the allDivs array. In each loop iteration, we generate two random numbers for the left and top positions for each div. We then set the div's position to absolute and add the left and top values. Since the pieceBox div has its position set to relative, each of these divs will be positioned using the left and top values relative to pieceBox. A css class, imgDraggable, is also added to each div. This class name will be used while dragging and dropping pieces. Finally, the div is appended to pieceBox.

The next line uses the createSlices function again to create a DOM with empty divs and without any background image. The DOM created using this function will be inserted to the puzzleContainer div again. Note that false is passed as a parameter to the createSlices function this time. This is because we do not want any background image in puzzleContainer when the game starts. This will require some modification in the createSlices function.

Modify the createSlices function written earlier to match the following code:

function createSlices(useImage)
{
  var str = '';
  for(var i=0, top=0, c=0; i < rows; i++, top-=100)
  {
    for(var j=0, left=0; j<cols; j++, left-= 100, c++)
    {
      if(useImage)
      {
        str+= '<div style="background-position: ' + left + 'px ' + top +'px;" class="img" data-sequence="'+c+'">';
      }
      else 
      {
        str+= '<div style="background-image:none;" class="img imgDroppable">';
      }
      str+= '</div>';
    }
  }
  return str;
}

Note

Do not forget to change the function call in the first line inside the $(document).ready(function()) section. Make sure var sliceStr = createSlices(); is written as var sliceStr = createSlices(true);.

If the useImage argument for the createSlices function is set to true, the background image will be used. If it is false, no background image will be set but a class named imgDroppable will be added. This class will be used to attach event handlers to the places where the puzzle pieces will be dropped.

Finally, after preparing the DOM for the pieceBox and puzzleContainer divs, the Start button is hidden and the Reset button is displayed.

Reload the HTML page in your browser and you should see something resembling the following screenshot:

Starting the game

Reloading the page and clicking the Start button will display different positions of the puzzle pieces every time.

Handling events for puzzle pieces

To be able to move pieces and use the possible movements, we first need to add events. We will have to add two event handlers, one to make the puzzle pieces inside pieceBox draggable and second to make the puzzleContainer pieces droppable.

Inside the event handler of the Start button, add a new function call named addPuzzleEvents().

Outside the $(document).ready(function()) event handler, define the addPuzzleEvents function by writing the following code:

function addPuzzleEvents()
{
  $('.imgDraggable').draggable(
  {
    revert : 'invalid',
      start : function(event, ui ){
      var $this = $(this);
      if($this.hasClass('pieceDropped'))
      {
        $this.removeClass('pieceDropped');
        ($this.parent()).removeClass('piecePresent');
      }
    }
  });

  $('.imgDroppable').droppable({
    hoverClass: "ui-state-highlight",
    accept : function(draggable)
    {

      return !$(this).hasClass('piecePresent');
    },
    drop: function(event, ui) {
      var draggable = ui.draggable;
      var droppedOn = $(this);
      droppedOn.addClass('piecePresent');
      $(draggable).detach().addClass('pieceDropped').css({
        top: 0,
        left: 0, 
        position:'relative'
      }).appendTo(droppedOn);

      checkIfPuzzleComplete();
    }
  });

}

There are two important points to be remembered here. Whenever a draggable puzzle piece is dropped on a droppable space, a CSS class named pieceDropped will be added to that draggable piece , which will indicate that the puzzle piece has been dropped. Another CSS class, piecePresent, will be added to the droppable space on which the piece is dropped. The presence of the piecePresent CSS class on a space will indicate that the space already has a piece dropped on it and we will disallow dropping any other draggable pieces on it.

All the puzzle pieces in pieceBox have a CSS class, imgDraggable, applied to them. We initialized the draggable component for all such pieces. While initializing, we provided two options for the draggable component. The first option is revert, which we set to invalid. As you may recall from Chapter 1, Designing a Simple Quiz Application, invalid means that a draggable piece will revert to its original position if it has not been dropped on any space. This also means when a piece is dropped inside puzzleContainer, you will not be able to place it back inside pieceBox.

Secondly, we added a start event handler to the piece. This event handler is called when the dragging begins. In the preceding code, we check whether the element being dragged has the pieceDropped class applied to it. If the pieceDropped class is not present on it, it means the piece is still inside pieceBox and has not been dropped in puzzleContainer yet.

If the pieceDropped class has been applied to the element, it means the puzzle piece was already dropped and it is being dragged inside puzzleContainer only. In this case, we want to allow the puzzle piece to be dropped onto other droppables spaces present inside puzzleContainer. Therefore, we remove the pieceDropped class from the draggable piece. In the next line, we also remove the piecePresent class from its parent droppable because we want the parent droppable to accept other draggable items.

Next, we will prepare the droppable space. In puzzleContainer, we have 16 different divs, which are used to accept the puzzle pieces. All of these have the imgDroppable CSS class applied to them. We initialize the droppable component using for all elements that have the imgDroppable class. While initializing, we provide three options, which are as follows:

  • hoverClass: In this option, we can specify the name of any CSS class, and it will be applied to the droppable element when a draggable element will be over it. Note that the class name will only be applied when an accepted draggable element is over the droppable element. In the preceding code, we used the ui-state-highlight class, which is available by default in jQueryUI themes.
  • accept: This option specifies which draggable elements can be dropped on to a droppable space. Either a jQuery selector or a function can be provided. We are using a function here to check whether the current droppable space already has a draggable element dropped in it or not. If the current droppable already has the piecePresent class, we return false, which means that the draggable element will not be allowed to drop on the current droppable space.
  • drop: This event takes place once an accepted draggable element (described in the previous bullet point) is dropped onto a droppable space. Once the draggable is dropped, we add the piecePresent CSS class to the droppable. We also want the dragged puzzle piece to fit to the parent droppable completely. For this, we remove the draggable element from the DOM using jQuery's detach method. Then we add a CSS class, pieceDropped, to this droppable space. We set its left and top positions to 0 and position to relative. Finally, we append it to the parent droppable. The CSS properties specified with it fit it to its parent droppable.

After each drop, we call the checkIfPuzzleComplete function to check whether the puzzle has been solved.

Checking for puzzle completion

Every time a piece is dropped inside puzzleContainer, we will have to check whether all the pieces are in the correct order or not. To do this, we will create a function named checkIfPuzzleComplete. This function will be called from the drop event of the droppables. Define this function as shown in the following code:

function checkIfPuzzleComplete()
{
  if($('#puzzleContainer div.pieceDropped').length != 16)
  {
    return false;
  }
  for(var i = 0; i < 16; i++)
  {
    var puzzlePiece = $('#puzzleContainer div.pieceDropped:eq('+i+')');
    var sequence = parseInt(puzzlePiece.data('sequence'), 10);
    if(i != sequence)
    {
      $('#message').text('Nope! You made the kitty sad :(').show();
      return false;
    }
  }
  $('#message').text('YaY! Kitty is happy now :)').show();
  return true;
}

It doesn't make any sense to check the puzzle if all 16 pieces have not been placed inside puzzleContainer. Since each puzzle piece dropped inside puzzleContainer will have a pieceDropped CSS class, we find out how many div elements with pieceDropped classes are present. If they are less than 16, we can assume that all pieces have not been placed inside the puzzle and return false from the function. If all 16 pieces are present inside puzzleContainer, we proceed to next step.

You may remember that we assigned a data-sequence attribute to each puzzle piece. In a correctly solved puzzle, all div elements will be in a sequence, which means their data-sequence attributes will have values from 0 to 15 in ascending order. Similarly, in an incorrectly solved puzzle, the data-sequence attributes of all div elements will still have values from 0 to 15, but they will not be in order.

The for loop checks the mentioned condition. We are running a loop from 0 to 15. Each iteration of the loop picks a div element from puzzleContainer whose index is equal to current loop value. The eq jQuery function is used for this purpose. The sequence value for this div element is then retrieved and compared to the current loop value. If any of the values inside loop does not match this value, it will mean that the puzzle pieces are not in a sequence. In this case, we display the Nope! You made the kitty sad :( message inside the div with the message ID, and exit from the function.

If the loop completes all iterations, it means that all puzzle pieces are in order. Then we display the YaY! Kitty is happy now :) message and return from the function. A correctly solved puzzle will resemble the following screenshot:

Checking for puzzle completion

Resetting the puzzle

To reset the puzzle, all we need to do is create the pieces again and fill puzzleContainer with them. Write the following code inside the $(document).ready(function()) handler to handle the reset button events:

$('#reset').on('click', function()
{
  var sliceStr = createSlices(true);
  $('#puzzleContainer').html(sliceStr);
  $('#pieceBox').empty();
  $('#message').empty().hide();
  $(this).hide();
  $('#start').show();
});

In the preceding code, we used the createSlices function with the true parameter and inserted the generated HTML inside puzzleContainer. Next, we emptied the pieceBox. The success or error message displayed earlier is also hidden. Finally, the Reset button is hidden and the Start button is displayed again.

Note

We do not need to call the addPuzzleEvents function to add drag and drop events. This is because the events were already attached to the DOM the first time the Start button was clicked.