//
// Function droppable() is the constructor for an ARIA drop-target widget. The widget is responsible
// for processing drop events over a target. It listens for and processes custom events from draggable
// widgets on the page. Events listened for are: grab, drag, and drop.
//
// A droppable widget will trigger targetEnter and targetLeave events when a draggable is moved within
// the tolerance zone of the droppable and then moved out of it again.
//
// @param (id string) id is the html id of the element to make into a drop target
//
// @param (maxItems integer) maxItems is the maximum number of items the drop target should accept
//
// @param (available boolean) available is the initial availability for the target. False if not accepting
// draggables.
//
// @return N/A
//
function droppable(id, maxItems, available) {
if (this.dropType == 'none' || available == false) {
this.available = false; // do not accept draggables
}
else {
this.available = true; // accept draggables
}
this.maxItems = maxItems; // the maximum number of draggables widget can hold
this.$items = null; // an array of jQuery objects for the draggables contained by the target
this.activeDraggable = null; // activeDraggable is the currently grabbed draggable
this.over = false; // true if a draggable is over the droppable. Set by targetEnter and targetLeave.
// bind event handlers
this.bindHandlers();
} // end droppable() constructor
//
// Function setAvailable() is a member function to set the availability state of the droppable.
//
// @param (available boolean) available is true if droppable should accept draggables; false if not
//
// @return N/A
//
droppable.prototype.setAvailable = function(available) {
this.available = available;
if (available == true) {
// restore the dropeffect type
this.$id.attr('aria-dropeffect', this.dropType);
}
else {
// store the dropeffect type
this.dropType = this.$id.attr('aria-dropeffect');
// set the dropeffect type to 'none' (i.e. do not accept draggables)
this.$id.attr('aria-dropeffect', 'none');
}
}
//
// Function makeCopy() is a member function to create a copy of the dragged object. This
// function will return the original dragged object to its starting position if the
// boolean parameter is true.
//
// @return N/A
//
droppable.prototype.makeCopy = function() {
var $copy = this.activeDraggable.$id.clone(); // copy the draggable
// remove the grabbed class
$copy.removeClass('grabbed');
if (this.$items.length == this.maxItems) {
// target is full, modify the aria-dropeffect to be 'none'
this.$id.attr('aria-dropeffect', 'none');
this.available = false;
}
} // end makeCopy()
//
// Function bindHandlers() is a member function to bind event handlers for a droppable.
//
// @return N/A
//
droppable.prototype.bindHandlers = function() {
var thisObj = this;
// bind handler for a grab event
this.$id.bind('grab', function(e, draggable) {
return thisObj.handleGrab(e, draggable);
});
// bind handler for a drag event
this.$id.bind('drag', function(e, draggable) {
return thisObj.handleDrag(e, draggable);
});
// bind handler for a drop event
this.$id.bind('drop', function(e, draggable) {
return thisObj.handleDrop(e, draggable);
});
} // end bindHandlers()
//
// Function handleGrab() is a member function to process grab events triggered by a draggable
//
// @param (e object) e is the event object;
//
// @param (draggable object) draggable is the draggable widget that triggered the event
//
// @return (boolean) Returns true;
//
droppable.prototype.handleGrab = function(e, draggable) {
this.activeDraggable = draggable;
if (this.available) {
this.$id.addClass('target-available');
}
return true;
} // end handleGrab()
//
// Function handleDrag() is a member function to process drag events triggered by a draggable
//
// @param (e object) e is the event object;
//
// @param (draggable object) draggable is the draggable widget that triggered the event
//
// @return (boolean) Returns true;
//
droppable.prototype.handleDrag = function(e, draggable) {
var thisObj = this;
if (this.available == false) {
// do nothing
return true;
}
// draggable is withing target bounds: trigger targetEnter event.
//
// Note: timeout is used to handle a race condition when dragging via keyboard where
// targetLeave would sometimes be triggered after targetEnter.
//
window.setTimeout(function() {
$.event.trigger('targetEnter', thisObj);
}, 10);
//
// Function handleDrop() is a member function to process drop events triggered by a draggable
//
// @param (e object) e is the event object;
//
// @param (draggable object) draggable is the draggable widget that triggered the event
//
// @return (boolean) Returns true;
//
droppable.prototype.handleDrop = function(e, draggable, blerf) {
if (this.over == true && draggable.validDrop == true) {
switch (this.dropType) {
case 'copy': {
this.makeCopy();
break;
}
case 'move': {
break;
}
}
}
// remove hover and available styling
this.$id.removeClass('target-hover target-available');
this.activeDraggable = null;
this.over = false;
return true;
} // end handleDrop()
//
/////////////////////////////////// End droppable widget definition ///////////////////////////////////////
/////////////////////////////////// Begin draggable widget definition ///////////////////////////////////////
// Function draggable() is the constructor for an ARIA drag-and-drop widget.
//
// The draggable class listens for and processes custom events from droppable widgets (representing drop
// targets). Events listened for are: targetEnter and targetLeave. If drop targets are registered with (i.e.
// passed to) the draggable, a valid drop may not be performed until a targetEnter event is received.
//
// If grabbing is done via the keyboard, it must be dragged and dropped with the keyboard. Keyboard dragging
// will jump to the first registered drop target with tabindex=0 -- giving it focus.
//
// draggable() requires:
//
// - The html ID of a draggable object
//
// - A jQuery collection of objects pointing to drop targets
// (pass null if not defining drop targets)
//
// - Values specifying the X and Y drag tolerance (i.e. amount mouse must move
// before drag event begins. Pass 0 for no tolerance.
//
// - A boolean specifying if a drag helper should be shown.
//
// @param (id string) id is the HTML id of the object to make draggable
//
// @param ($targets object) $targets is a jQuery collection pointing to elements to use
// as drop targets. This parameter may be null.
//
// @param (useActive boolean) true if target is determined by aria-activedescendent; false
// if determined by taborder.
// @param (dragTol_x integer) dragTol_x is the horizontal pixel distance mouse must move
// to trigger a drag event.
//
// @param (dragTol_y integer) dragTol_y is the vertical pixel distance mouse must move
// to trigger a drag event.
//
// @param (useHelper boolean) useHelper is true if a helper object should be used.
//
// @param (enabled boolean) enabled is true if the draggable should be enabled initially
//
// @return N/A
//
function draggable(id, $targets, useActive, dragTol_x, dragTol_y, useHelper, enabled) {
// get the object's margins
this.margins = {
top: (parseInt(this.$id.css("marginTop"),10) || 0),
left: (parseInt(this.$id.css("marginLeft"),10) || 0)
};
// get the starting location of the object (adjusted for margins). This will
// be used to return the draggable to its starting position if drop targets
// have been specified and the user drops it elsewhere.
//
this.startPos = {
x: this.$id.offset().left - this.margins.left,
y: this.$id.offset().top - this.margins.top
}
// get the offset to the object's center point
this.center = {
x: (this.$id.outerWidth() / 2),
y: (this.$id.outerHeight() / 2)
};
////////////////////////////////////////
//
// The following three properties (clickPos, curPos, and prevPos) will be updated
// as the draggable object is dragged around the screen. If useHelper is true,
// these values will pertain to the position of the drag helper, as the original
// object will not be moved. If useHelper is false, the values will pertain to the
// draggable object itself.
//
// clickPos is the position within the object where a click occured.
// This position is set in the mousedown handler.
this.clickPos = {x: -1, y: -1};
// copy the starting position into a variable to give the current position. This
// position will be updated as the object is dropped in other locations.
this.curPos = {
x: this.startPos.x,
y: this.startPos.y
};
// prevPos is the location of the object after the last mousemove event.
this.prevPos = { x: -1, y: -1};
// store any passed drop targets
this.$targets = $targets;
this.useActive = useActive; // changes how the drag is handled for keyboard dragging
this.activeTarget = undefined; // if target specified, points to one under draggable
this.validDrop = true; // false if there are drop targets and draggable is not over one.
// set widget flags
this.grabbed = false; // true if object has been grabbed
this.clicked = false; // true if object grabbed by a mouse click
this.dragging = false; // true if object drag is occuring
// bind event handlers
this.bindHandlers();
} // end draggable() constructor
//
// Function bindHandlers() is a member function to bind event handlers to the draggable object
//
// @return N/A
//
draggable.prototype.bindHandlers = function() {
//
// Function enable() is a member function to set the draggable to an enabled state (i.e. able to be grabbed
// and dragged).
//
// @return N/A
//
draggable.prototype.enable = function() {
this.enabled = true;
this.$id.attr('aria-grabbed', 'false');
} // end enable()
//
// Function disable() is a member function to set the draggable to a disabled state.
//
// @return N/A
//
draggable.prototype.disable = function() {
this.enabled = false;
this.$id.attr('aria-grabbed', '');
} // end disable()
//
// Function createHelper() is a member function to create a clone of the draggable for use as a helper.
// The function creates the clone, modifies the appropriate parameters, appends the clone to the
// document, and binds necessary event handlers,
//
// @return N/A
//
draggable.prototype.createHelper = function() {
// bind mouse handlers if grabbed with the mouse, key handlers if via keyboard
if (this.clicked == true) {
// bind event handlers
this.$helper.mousemove(function(e) {
return thisObj.handleMouseMove(e);
});
//
// Function destroyHelper() is a member function to remove the drag helper from the DOM.
//
// @return N/A
//
draggable.prototype.destroyHelper = function() {
this.$helper.remove();
this.$helper = undefined;
} // end destroyHelper
//
// function doGrab() is a member function to process a grab event. It adds grabbed styling to the
// grabbable object, creates a helper (if useHelper is true), and binds event handlers needed to move
// either the object or the helper.
//
// @return N/A
//
draggable.prototype.doGrab = function() {
var thisObj = this;
// Only process if the object is enabled and has not already been grabbed
if (this.enabled == true && this.grabbed == false) {
// add the grabbed class
this.$id.addClass('grabbed');
// Initialize prevPos to be the start position of the object
if (this.clicked == true) {
this.prevPos = {
x: this.clickPos.x,
y: this.clickPos.y
};
}
else {
this.prevPos = {
x: this.startPos.x,
y: this.startPos.y
};
}
// if using a helper, create it; else, bind event handlers to the
// grabbable object
//
if (this.useHelper == true) {
this.createHelper();
// set focus on the helper
this.$helper.focus();
}
else if (this.clicked == true) { // only bind mouse handlers if grabbing via mouse
// bind a mouseup handler
this.$id.mouseup(function(e) {
return thisObj.handleMouseUp(e);
});
// if there are targets, set validDrop to false.
if (this.$targets) {
this.validDrop = false;
}
// set the grabbed flag
this.grabbed = true;
// set the aria-grabbed property
this.$id.attr('aria-grabbed', 'true');
// trigger a grab event to notify droppables
$.event.trigger('grab', this);
}
} // end doGrab()
//
// Function doDrag() is a member function to process a drag event. The function checks to see
// if the object is in the dragging state (this will always be the case if doDrag() is called
// following a key event). If not dragging, the function checks that the drag tolerance has been
// exceeded and enters the dragging state if this is so. If dragging, the function updates the
// pixel position of the object and moves it.
//
// @param (xPos float) xPos is the horizontal pixel position of the object being dragged
//
// @param (yPos float) yPos is the vertical pixel position of the object being dragged
//
// @return N/A
//
draggable.prototype.doDrag = function(xPos, yPos) {
var $id = this.$id; // initially set $id to point to the draggable
if (this.useHelper == true) {
$id = this.$helper; // set $id to point to the helper
}
// if not dragging, check to see if mouse has moved beyond drag tolerance
if (this.dragging == false) {
// if the mouse has been moved beyond the drag tolerance, begin a drag event
if (Math.abs(xPos - this.clickPos.x) > this.dragTol.x
|| Math.abs(yPos - this.clickPos.y) > this.dragTol.y) {
// set the dragging flag
this.dragging = true;
// change the position of the draggable to absolute
$id.css('position', 'absolute');
// set the initial position
$id.css('left', this.curPos.x);
$id.css('top', this.curPos.y);
}
}
else { ////// dragging ///////
// update the horizontal position
this.curPos.x += xPos - this.prevPos.x;
// update the vertical position
this.curPos.y += yPos - this.prevPos.y;
// change the position of the draggable to absolute
$id.css('position', 'absolute');
// move the draggable
$id.css('left', this.curPos.x);
$id.css('top', this.curPos.y);
// update prevPos to be the current mouse location
this.prevPos = {
x: xPos,
y: yPos
};
}
// trigger the drag event to notify droppables
$.event.trigger('drag', this);
} // end doDrag()
//
// Function doDrop() is a member function to process a drop event for the draggable object. The function
//
// @return N/A
//
draggable.prototype.doDrop = function() {
// unbind the mouse event handlers
if (this.useHelper == true) {
this.$helper.unbind('mousemove');
this.$helper.unbind('mouseup');
}
else {
this.$id.unbind('mousemove');
this.$id.unbind('mouseup');
}
if (this.dragging == true) {
// if this is an invalid drop, return the draggable to its starting position
if (this.validDrop == false) {
if (this.useHelper == true) {
this.$helper.animate({
left: this.startPos.x,
top: this.startPos.y
}, 'fast');
}
else {
// return draggable object (or it's drag helper) to the starting position
this.$id.animate({
left: this.startPos.x,
top: this.startPos.y
}, 'fast');
}
// return focus to the draggable object
this.$id.focus();
break;
}
case "move": {
if (this.useHelper == true) {
// make certain the draggable has absolute positioning.
this.$id.css('position', 'absolute');
// move the draggable
this.$id.animate({
left: this.curPos.x,
top: this.curPos.y
}, 'fast', function() {
// reset the position to relative and remove
// top and left specification.
$(this).css('top', '').css('left', '').css('position', 'relative');
});
// update startPos to be the current position
this.startPos = {
x: this.curPos.x,
y: this.curPos.y
};
}
else {
// draggable is already where we want it
// reset the draggable CSS positioning
this.$id.css('position', 'relative');
this.$id.css('top', '');
this.$id.css('left', '');
}
break;
}
} // end switch
}
// reset the click position
this.clickPos = { x: -1, y: -1 };
// reset the flags
this.dragging = false;
}
if (this.useHelper == true) {
// destroy the helper
this.destroyHelper();
}
// remove the grabbed class from the draggable object
this.$id.removeClass('grabbed');
// set aria-grabbed property to false
this.$id.attr('aria-grabbed', 'false');
// trigger the drop event to notify droppables
$.event.trigger('drop', this);
//
// function abandoneDrag() is a member function called by the application to abandon the
// current drag and drop operation.
//
// @return N/A
//
draggable.prototype.abandonDrag = function() {
this.validDrop = false;
this.doDrop();
} // end abandonDrag()
// Function handleMouseDown() is a member function to processmousedown events for the draggable object.
// This function stores where the original click occured within the object and binds a mousemove handler.
//
// @param (e object) e is the event object
//
// @return (boolean) returns false
//
draggable.prototype.handleMouseDown = function(e) {
var thisObj = this;
// set the clicked flag
this.clicked = true;
// store the click position
this.clickPos = {
x: e.pageX,
y: e.pageY
};
// do grab processing
this.doGrab();
e.stopPropagation();
return false;
} // end handleMouseDown()
// Function handleMouseUp() is a member function to process mouseup events for the draggable object.
// This function performs the drop operation for the object. If the draggable is not dropped on a drop
// target, it is returned to it's starting position. After the drop, the mousemove handler is unbound.
//
// @param (e object) e is the event object
//
// @return (boolean) returns false
//
draggable.prototype.handleMouseUp = function(e) {
// perform the drop processing
this.doDrop();
e.stopPropagation();
return false;
} // end handleMouseUp()
//
// Function handleMouseMove() is a member function to process mouse move events. This function
// will set the dragging flag and perform the drag if the mouse is moved beyond the drag tolerance.
//
// @param (e object) e is the event object
//
// @return (boolean) returns false
//
draggable.prototype.handleMouseMove = function(e) {
//
// Function handleKeyDown() is a member function to process keydown events. This function
// will handle all key events for the object, including placing it in grabbed mode, drag mode,
// and dropped mode.
//
// Dragging with the keyboard will move focus to the first drop target with a tabindex=0.
//
// @param (e object) e is the event object
//
// @return (boolean) Returns true if propagating; false if not.
//
draggable.prototype.handleKeyDown = function(e) {
var target = null;
if (e.altKey || e.ctrlKey
|| (e.shiftKey && e.keyCode != this.keys.tab)) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.tab: {
if (this.grabbed == false) {
// do nothing;
return true;
}
if (!this.$targets) {
// adandon the drag
this.abandonDrag();
}
else {
if (this.useActive == true) {
// get target from aria-activedescendant attribute
$target = $('#' + this.$targets.attr('aria-activedescendant'));
// give the passed element focus
this.$targets.focus();
}
else {
// get target from tab order
$target = this.$targets.filter('[tabindex=0]');
// give the target focus
$target.focus();
}
// set the dragging flag to true
this.dragging = true;
// Drag the draggable to the drop target
this.doDrag($target.offset().left, $target.offset().top);
$target = null;
e.stopPropagation();
return false;
}
return true;
}
case this.keys.esc: {
if (this.grabbed == true) {
// adandon the drag
this.abandonDrag();
// if useHelper is true, return
// focus to the draggable
if (this.useHelper == true) {
this.$id.focus();
}
}
e.stopPropagation();
return false;
}
case this.keys.enter:
case this.keys.space: {
// if useHelper is true, return
// focus to the draggable
if (this.useHelper == true) {
this.$id.focus();
}
// reset the clicked flag
this.clicked = false;
}
e.stopPropagation();
return false;
}
case this.keys.left:
case this.keys.up:
case this.keys.right:
case this.keys.down: {
e.stopPropagation();
return false;
}
} // end switch
return true;
} // end handleKeyDown()
//
// Function handleKeyPress() is a member function to consum keypress events. This function
// is necessary to prevent some browsers (such as Opera) from performing window manipulation
// on keypress events.
//
// @param (e object) e is the event object
//
// @return (boolean) Returns true if propagating; false if not.
//
draggable.prototype.handleKeyPress = function(e) {
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.esc:
case this.keys.enter:
case this.keys.left:
case this.keys.up:
case this.keys.right:
case this.keys.down: {
e.stopPropagation();
return false;
}
} // end switch
return true;
} // end handleKeyPress()
//
// Function handleTargetEnter() is a member function to process targetEnter events triggered by droppables
//
// @param (e object) e is the event object
//
// @param (droppable object) droppable is the droppable triggering the event
//
// @return (boolean) Returns true
//
draggable.prototype.handleTargetEnter = function(e, droppable) {
//
// Function handleTargetLeave() is a member function to process targetLeave events triggered by droppables
//
// @param (e object) e is the event object
//
// @param (droppable object) droppable is the droppable triggering the event
//
// @return (boolean) Returns true
//
draggable.prototype.handleTargetLeave = function(e, droppable) {
var game = new tictactoe('grid1', 'startButton', 'out', 3, 3);
}); // end ready()
//
// Function tictactoe() defines a class to implement a tic-tac-toe game. The board is
// an ARIA grid widget. The game listens to the grab and drop events triggered by
// the draggables and the targetEnter and targetLeave events triggered by the droppables.
//
// @param (boardID string) boardID is the html id of the table to attach to.
//
// @param (startID string) startID is the html id of the start button to use.
//
// @param (msgID string) msgID is the html id of the message box to use.
//
// @param (numRows integer) numRows is the number of rows in the grid
//
// @param (numCols integer) numCols is the number of columns in the grid
//
// @return N/A
//
function tictactoe(boardID, startID, msgID, numRows, numCols) {
// define widget properties
this.$id = $('#' + boardID);
this.$startButton = $('#' + startID);
this.$msgBox = $('#' + msgID);
this.keys = new keyCodes();
this.$rows = this.$id.find('tr');
this.$cells = this.$id.find('td');
this.$active = $('#' + this.$id.attr('aria-activedescendant')); // the cell to treat as active (e.g. focus)
this.targets = []; // an array of droppable widgets
var thisObj = this;
// create droppable instances for each dropTarget found
this.$cells.each(function(index) {
thisObj.targets[index] = new droppable($(this).attr('id'), 1, false);
});
this.numRows = numRows;
this.numCols = numCols;
this.player1 = new draggable('ex', this.$id, true, 10, 10, false, false);
this.player2 = new draggable('oh', this.$id, true, 10, 10, false, false);
this.curPlayer = null; // set to the current player piece widget
this.activeDraggable = null; // set to the grabbed draggable upon receiving a drag event
this.activeTarget = null; // set to the droppable under the draggable
//
// Function updateUser() is a member function to output a status message to the message box
//
// @param (msg string) msg is the message to output
//
// @return N/A
//
tictactoe.prototype.updateUser = function(msg) {
this.$msgBox.html(msg);
} // end updateUser
//
// Function clearBoard() is a member function to remove any pieces from the game board and to
// reset the labels for the grid cells.
//
// @return N/A
//
tictactoe.prototype.resetBoard = function() {
var thisObj = this;
// empty the cells
this.$cells.find('*').not('p').remove();
// set the active descendant to be the first cell
this.$id.attr('aria-activedescendent', this.$cells.first().attr('id'));
// For each row, find the label for each cell and reset its contents
this.$rows.each(function(rowIndex) {
$(this).find('p').each(function(colIndex) {
if (rowIndex == 1 && colIndex == 1) {
// the center cell should only be labelled as 'center'
$(this).text(thisObj.colLabels[colIndex] + ' Cell is empty');
}
else {
$(this).text(thisObj.rowLabels[rowIndex] + ' ' + thisObj.colLabels[colIndex] + ' Cell is empty');
}
});
});
// remove win highlight from cells
this.$cells.removeClass('win');
// reset the moveCount and end game flags
this.moveCount = 0;
this.winner = '';
//
// Function endGameCheck() is a member function to check for an end of game condition. A player may only win
// if his/her last move makes 3 in a row; therefor, this function checks possible wins from the last position
// played. The worst case scenario is if the last move is the center square.
//
// @return N/A
//
tictactoe.prototype.endGameCheck = function() {
var $row = this.activeTarget.$id.parent(); // the row containing the square played in
var rowNdx = $row.index(); // the index of the row played in
var colNdx = this.activeTarget.$id.index(); // the index of the column played in
var lastPiece = ''; // the name of the piece that was played ('ex' or 'oh')
var $squares = null; // contains a list of squares that contain matching pieces
// Determine which piece was played last
if (this.activeDraggable.$id.hasClass('ex') == true) {
lastPiece = 'ex';
}
else {
lastPiece = 'oh';
}
/////////////// check the row //////////////////////
// Do a find for all matching pieces in the row
// played in. If the count equals the number of columns,
// we have a winner.
if ($row.find('div.' + lastPiece).length == this.numCols) {
// add the win styling to the row cells
$row.find('td').addClass('win');
// set winner
this.winner = lastPiece;
}
///////////// check the column ////////////////////
// Iterate through the rows, building a list of
// matching squares in the column last played in.
this.$rows.each(function(index) {
var $curSquare = $(this).find('td').eq(colNdx);
var $div = $curSquare.find('div');
// if there is a piece in the square, and that
// piece matches the last one played, add the square
// to the list of squares.
if ($div) {
// if the count equals the number of rows, we have a winner
if ($squares.length == this.numRows) {
// add the win styling to the squares
$squares.addClass('win');
// set winner
this.winner = lastPiece;
}
// reset $squares
$squares = null;
/////////// check the diagonal (ul to lr) ////////////////////
// check the three square where the row and column indices match.
// If the square contains a matching piece, add it to the list.
for (ndx = 0; ndx < this.numRows; ndx++) {
var $curSquare = this.$rows.eq(ndx).find('td').eq(ndx);
var $div = $curSquare.find('div');
// if there is a piece in the square, and that
// piece matches the last one played, add the square
// to the list of squares.
if ($div) {
if ($div.hasClass(lastPiece) == true) {
if ($squares) {
$squares = $squares.add($curSquare);
}
else {
$squares = $curSquare;
}
}
}
}
// if the count equals the number of rows, we have a winner
if ($squares) {
if ($squares.length == this.numRows) {
// add the win styling to the squares
$squares.addClass('win');
// set winner
this.winner = lastPiece;
}
// reset $squares
$squares = null;
}
/////////// check the other diagonal (ur to ll) ////////////////////
// check the three square where the row and column are opposites.
// If the square contains a matching piece, add it to the list.
for (ndx = 0; ndx < this.numRows; ndx++) {
var $curSquare = this.$rows.eq(ndx).find('td').eq(2 - ndx);
var $div = $curSquare.find('div');
// if there is a piece in the square, and that
// piece matches the last one played, add the square
// to the list of squares.
if ($div) {
if ($div.hasClass(lastPiece) == true) {
if ($squares) {
$squares = $squares.add($curSquare);
}
else {
$squares = $curSquare;
}
}
}
}
// if the count equals the number of rows, we have a winner
if ($squares) {
if ($squares.length == this.numRows) {
// add the win styling to the squares
$squares.addClass('win');
// set winner
this.winner = lastPiece;
// the game is over
return true;
}
}
if (this.winner.length > 0) {
return true;
}
// there is a draw if the piece count is equal to
// the number of cells in the grid
if (this.moveCount == this.$cells.length) {
// the game is over
return true;
}
// game is not over yet
return false;
} // end endGameCheck()
//
// Function bindHandlers() is a member function to bind event handlers for the board.
//
// @return N/A
//
tictactoe.prototype.bindHandlers = function() {
// bind a click handler for the start button
this.$startButton.click(function(e) {
return thisObj.handleStartClick(e);
});
} // end bindHandlers()
//
// Function handleKeyDown() is a member function to process keydown events for
// the gameboard
//
// @param (e object) e is the event object
//
// @param ($id object) $id is the jquery object of the cell triggering event
//
// @return (boolean) Returns false if consuming event; true if propagating
//
tictactoe.prototype.handleKeyDown = function(e, $id) {
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.tab: {
if (this.activeDraggable) {
this.activeDraggable.abandonDrag();
}
// tab must propagate
return true;
}
case this.keys.esc: {
if (this.activeDraggable) {
this.activeDraggable.abandonDrag();
}
e.stopPropagation();
return false;
}
case this.keys.enter:
case this.keys.space: {
if (this.activeTarget == null) {
// player attempted to drop a piece on
// and occupied space
this.updateUser('That space is occupied!');
}
else if (this.activeDraggable) {
this.activeDraggable.doDrop();
}
e.stopPropagation();
return false;
}
case this.keys.left: {
if (this.$active.index() > 0) {
this.$active = this.$active.prev();
//
// Function handleKeyPress() is a member function to consume keypress events for
// the gameboard. This handler is necessary to prevent unwanted window manipulation
// in browsers that process on keypress rather than keydown (such as Opera).
//
// @param (e object) e is the event object
//
// @param ($id object) $id is the jquery object of the cell triggering event
//
// @return (boolean) Returns false if consuming event; true if propagating
//
tictactoe.prototype.handleKeyPress = function(e, $id) {
if (e.altKey || e.ctrlKey || e.shiftKey) {
// do nothing
return true;
}
switch (e.keyCode) {
case this.keys.esc:
case this.keys.enter:
case this.keys.space:
case this.keys.left:
case this.keys.up:
case this.keys.right:
case this.keys.down: {
e.stopPropagation();
return false;
}
}
return true;
} // end handleKeyPress()
//
// Function handleGrab() is a member function to process grab events triggered by
// a draggable. This function stores the active draggable so the grid may manipulate
// its position.
//
// @param (e object) e is the event object
//
// @param (draggable object) draggable is the draggable object triggering event
//
// @return (boolean) Returns true
//
tictactoe.prototype.handleGrab = function(e, draggable) {
this.activeDraggable = draggable;
return true;
} // end handleGrab()
//
// Function handleDrop() is a member function to process Drop events triggered by
// a draggable. This function sets the stored draggable to null.
//
// @param (e object) e is the event object
//
// @param (draggable object) draggable is the draggable object triggering event
//
// @return (boolean) Returns true
//
tictactoe.prototype.handleDrop = function(e, draggable) {
if (draggable.validDrop == true) {
// drop was valid, modify the cell label
var piece = this.activeDraggable.$id.attr('id');
var row = this.activeTarget.$id.parent().index();
var col = this.activeTarget.$id.index();
this.$active = this.activeTarget.$id;
// Make this cell the active descendant
this.$id.attr('aria-activedescendant', this.$active.attr('id'));
// modify the label
if (row == 1 && col == 1) {
this.activeTarget.$id.find('p').text(this.colLabels[col]
+ ' Cell contains an ' + piece);
}
else {
this.activeTarget.$id.find('p').text(this.rowLabels[row] + ' '
+ this.colLabels[col] + ' Cell contains an ' + piece);
}
// Remove the id attribute of the piece, as it is now non-unique (i.e. invalid markup)
// and is not necessary. Also remove the piece copy from the tab order. Use jQuery
// chaining for speed.
this.activeTarget.$id.find('div.piece').removeAttr('id').attr('tabindex', '-1');
//increment the moveCount
this.moveCount++;
// check for end of game
if (this.endGameCheck() == false) {
// swap players
if (this.curPlayer == this.player1) {
if (this.winner.length > 0) {
this.updateUser(this.winner + ' has won!');
}
else {
this.updateUser('Tie game.');
}
// set focus on start button
this.$startButton.focus();
}
}
else {
this.updateUser('You must place an \'' + this.curPlayer.$id.attr('id') + '\' in an empty space.');
}
return true;
} // end handleDrop()
//
// Function handleTargetEnter() is a member function to process a targetEnter event triggered
// by a droppable. This function stores the active droppable so the grid may manipulate
// it.
//
// @param (e object) e is the event object
//
// @param (droppable object) droppable is the droppable object triggering event
//
// @return (boolean) Returns true
//
tictactoe.prototype.handleTargetEnter = function(e, droppable) {
this.activeTarget = droppable;
return true;
} // end handleTargetEnter()
//
// Function handleTargetLeave() is a member function to process a targetLeave event triggered
// by a droppable. This function resets the stored activeTarget.
//
// @param (e object) e is the event object
//
// @param (droppable object) droppable is the droppable object triggering event
//
// @return (boolean) Returns true
//
tictactoe.prototype.handleTargetLeave = function(e, droppable) {
this.activeTarget = null;
return true;
} // end handleTargetLeave()
//
// Function handleStartClick() is a member function to process click events for the start
// button. This function resets the board and sets focus on the first player's piece.
//
// @param (e object) e is the event object
//
// @return (boolean) Returns false
//
tictactoe.prototype.handleStartClick = function(e) {
var thisObj = this;
// clear the game board
this.resetBoard();
// make the drop targets available
for (var ndx = 0; ndx < this.targets.length; ndx++) {
thisObj.targets[ndx].setAvailable(true);
}
// set the current player to be player 1
this.curPlayer = this.player1;
// enable the player1 piece and disable player2
this.player1.enable();
this.player2.disable();
// notify user that player one should take a turn
this.updateUser(this.curPlayer.$id.attr('id') + '\'s turn to play');
// set focus on the current player piece
this.player1.$id.focus();