Create a Crossword Puzzle in SharePoint with jQuery and SPServices

Web Design & Development

As part of a website redesign communication campaign, I was tasked with developing a web-based crossword puzzle for our intranet site (MOSS 2007). After some brainstorming I came up with a solution that seems to be effective and relatively easy to set up using a custom list to store the answers, some jQuery to enhance the interactivity of the crossword, and SPServices to submit the answers to the list. Check out a demo page of the crossword (it won’t actually submit because it’s just a standalone HTML page and it isn’t on a SharePoint server).

Create the Crossword

I started by creating a prototype of the crossword in Excel. This allowed me to get my grid and clue numbers figured out before any development. Crossword puzzles by convention are numbered starting from top-left and moving down to the bottom-right. In other words, the start of the first word in the top row will be number 1. It’s orientation determines if it is 1 Across or 1 Down. Some numbers will have both an Across and Down orientation. This is important to keep track of because you’ll need a list column for each clue response. For this demo I’m using a very simple crossword with just five clues.

Screenshot of crossword mockup in excel
Excel is a great tool for mocking up your crossword before you start coding. I recommend noting the clue numbers with comments.

Create the Custom List

Now that we know what clue numbers we’ll need and in what orientation, we can create the custom list. Once the list is created, add a single line of text column for each clue. I labeled them “[Number][Orientation]” for easy reference. For example, the column for clue 1 Across would be called “OneAcross.” If there is also a clue for 1 Down, you’d create another column called “OneDown.” Do this for all of the clues in the crossword.

Screenshot of crossword list columns in SharePoint.
Create a single line of text column for each response in your crossword.

Write the HTML for the Crossword

At the risk of offending semantic purists (with whom I usually agree) I decided to use a table to create the crossword grid. To start, create a simple table with a <div> inside each cell for every grid square in your crossword (e.g. if you have a 10×10 grid, we’ll need ten table rows with ten cells in each row). The  <div> ensures that the empty cells will display correctly with a border across all browsers once we add some CSS. You could do this in a Content Editor Web Part on a web part page or add it to a custom NewForm page for the Crossword list.

<table id="crossword" border="0" cellpadding="0" cellspacing="0">
	<tbody>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
	</tbody>
</table>

Add Clue Numbers

Now that we have the grid set up, it’s time to add the numbers for each clue. Start at the top row and find the first <td> that should contain the first letter of an answer. Inside the <div> add a <span>[ClueNumber]</span> where [ClueNumber] is the number of the clue. The first <span> added to the grid should be <span>1</span>. Do this for all remaining rows.

<table id="crossword" border="0" cellpadding="0" cellspacing="0">
	<tbody>
		<tr>
			<td><div><span>1</span></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div><span>2</span></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div><span>3</span></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div><span>4</span></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
	</tbody>
</table>

Add Some CSS

Before we get much further, it’d be a good idea to look at the table and make sure the clue numbers are in the right location. To make this easier, add some CSS to the page to format the table cells into squares with borders.

#crossword {
	color: #444;
	font-family: arial, sans-serif;
	font-size: 18px;
}
#crossword td {
	background-color: #fff;
	border: 1px solid #777;
	box-sizing: border-box;
	width: 26px;
	height: 26px;
	vertical-align: middle;
}
#crossword td div {
	position: relative;
	width: 24px;
	height: 24px;
}
#crossword td span {
	display: block;
	position: absolute;
	z-index: 2;
	font-size: .5em;
	line-height: 1em;
	top: 1px;
	left: 1px;
}
#crossword td.empty {
	background-color: #aaa;
}
#crossword td input {
	background-color: #fff;
	border: none;
	color: #777;
	display: block;
	font-family: arial, sans-serif;
	font-size: inherit;
	line-height: 22px;
	width: 22px;
	height: 22px;
	text-align: center;
	text-transform: uppercase;
}

The table should look something like this now:

Screenshot of crossword table basic grid.
With some CSS applied, you should now be able to view the table grid and make sure that the clue numbers are in the correct location.

Mark the Empty Cells

This isn’t a necessary step for the crossword to function, but for styling purposes I added a class of “empty” to the table cells that won’t contain a letter for a response. As with the clue numbers, start at the top row and add the class to each table cell that won’t contain a letter for a response.

<table id="crossword" border="0" cellpadding="0" cellspacing="0">
	<tbody>
		<tr>
			<td><div><span>1</span></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><span>2</span></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><span>3</span></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><span>4</span></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
			<td><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
	</tbody>
</table>

Add Text Inputs

The letters for the responses will be entered using text inputs. Once again starting at the top row, find the first <td> that should contain a letter for a response. Inside the <div> (and after the <span> if it’s present) add an <input type="text" maxlength="1" class="[clue]-[orientation]" /> where [clue]-[orientation] is replaced with the clue number and orientation that the input is used for (I always use hyphens for class names rather than camel case, but you can use your preferred convention). If the input is used for two responses, include two classnames—one for each clue number and orientation.

<table id="crossword" border="0" cellpadding="0" cellspacing="0">
	<tbody>
		<tr>
			<td><div><span>1</span><input type="text" maxlength="1" class="one-across one-down" /></div></td>
			<td><div><input type="text" maxlength="1" class="one-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="one-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="one-across" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><span>2</span><input type="text" maxlength="1" class="two-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="two-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="two-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="two-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><span>3</span><input type="text" maxlength="1" class="three-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><span>4</span><input type="text" maxlength="1" class="one-down four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="two-down four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="three-down four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="two-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="three-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="three-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="three-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="three-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
	</tbody>
</table>

Note: You could also add tabindex attributes to each input, but I couldn’t think of a good way to do this that takes into account both horizontal and vertical words. The default tab order isn’t terrible, and most users can always use the mouse to place the cursor in the desired input. I figured that messing with this too much might detract from the accessibility of the crossword (and we’ll be writing some jQuery to help navigating the inputs in the crossword using the keyboard arrow keys). I welcome any suggestions for this in the comments.

Write the Clues

I used a definition list (<dl>) for the crossword clues. You can use whatever markup you want here, but I think a definition list makes sense semantically, and it will be easy to style with CSS. Above the clues include a link or button to submit the form with an id of “crossword-submit.”

<p><input type="button" id="crossword-submit" value="Submit Your Crossword" /></p>

<h4 class="crossword-heading">Across</h4>
<dl class="crossword-questions">
	<dt>1.</dt>
	<dd>Abbreviation for the commercial version of SharePoint 2007.</dd>

	<dt>4.</dt>
	<dd>A JavaScript library built for interacting with SharePoint Web Services.</dd>
</dl>

<h4 class="crossword-heading">Down</h4>
<dl class="crossword-questions">
	<dt>1.</dt>
	<dd>The company that created SharePoint.</dd>

	<dt>2.</dt>
	<dd>“Write less, do more” with this popular JavaScript library.</dd>

	<dt>3.</dt>
	<dd>JavaScript can be added to a page using a ______ element.</dd>
</dl>

A Bit More CSS

The crossword clues won’t look that great without applying styles to them. I used the following CSS, but you can adjust it to fit your preference. Be sure to test any changes to this and the previous CSS for the crossword table in all browsers that your users might visit the page with. I’ve tested this in IE7+, Firefox 9, and Chrome 18.

.crossword-heading {
	clear: both;
}
.crossword-questions {
	overflow: hidden;
}
.crossword-questions dt {
	float: left;
}
.crossword-questions dt {
	clear: both;
	width: 2em;
	text-align: right;
}
.crossword-questions dd {
	margin-left: 3em;
}

Your crossword should now look something like this:

Screenshot of crossword and clues.
You should now have a crossword, submit button, and the crossword clues formatted.

Adding Interactivity

The crossword is usable as-is, but I wanted to provide some enhancement to make navigating the crossword by keyboard less cumbersome. For the record I’m using jQuery 1.7.1 and SPServices 0.7.1a (be sure to include them on your page before writing any other scripts for this demo). I included the following script inside a $( document ).ready() function. The script allows for navigation by arrow keys, prevents users from inputting a space character by accident, and automagically moves the cursor to the next input in the word they are currently typing (it isn’t foolproof, but in general it works OK). I included several comments within the script to explain what is happening.

// Add interactivity on mouseup and keyup for the inputs
$( '#crossword' ).find( 'input' ).mouseup( function( event ) {

	// Select the letter in the input so it can be replaced by simply typing another letter
	$( this ).select();
	event.preventDefault();
}).focus( function() {

	// Select the letter in the input so it can be replaced by simply typing another letter
	$( this ).select();
}).keyup( function( event ) {

	var code = event.keyCode || event.which;

	// Don't do anything if the Tab, Shift, Backspace, Space, or Delete keys are pressed.
	if ( code === 9 || code === 16 || code === 8 || code === 46 ) {
		return true;
	}

	// Automagically remove spaces when they are typed.
	else if ( code === 32 ) {
		$( this ).val( $( this ).val().replace(/\s/g, ''));
	}

	// Automatically focus on the next appropriate input once a letter or symbol is entered or an arrow key is pressed.
	// This is sort of a best-guess interaction based on where the user is typing.
	else {

		// Cache several variables that will be used to move focus to other inputs in the crossword.
		// If we try to get an input that doesn't exist, the script fails gracefully by simply not moving focus to another input.
		// Cache the class of the current input.
		var inputClass = $( this ).attr( 'class' );

		// Cache the table cell containing the current input.
		var cell = $( this ).closest( 'td' );

		// Cache the index of the current table cell.
		var colIndex = $( cell ).parent().children().index( cell );

		// Cache the next input to the right of the current input.
		var nextAcross = $( cell ).next().find( 'input' );

		// Cache the previous input to the left of the current input.
		var prevAcross = $( cell ).prev().find( 'input' );

		// Cache the value of the next input to the right of the current input.
		var nextAcrossVal = $( nextAcross ).val();

		// Cache the next input below the current input (this is why we need the column index of the current table cell).
		var nextDown = $( cell ).parent().next().find( 'td:eq(' + colIndex + ')' ).find( 'input' );

		// Cache the previous input above the current input (this is why we need the column index of the current table cell).
		var prevDown = $( cell ).parent().prev().find( 'td:eq(' + colIndex + ')' ).find( 'input' );

		// If the arrow keys are pressed, go to the appropriate input (if available).
		switch ( code ) {
			case 37: // left arrow
				prevAcross.focus();
				return false;
				break;
			case 38: // up arrow
				prevDown.focus();
				return false;
				break;
			case 39: // right arrow
				nextAcross.focus();
				return false;
				break;
			case 40: // down arrow
				nextDown.focus().addClass( "force-down" ); // If user pressed the down arrow, they probably want to continue typing a down response.
				return false;
				break;
			default:
		}

		// If the input is for both across AND down responses...
		if ( inputClass.indexOf( "across" ) !== -1 && inputClass.indexOf( "down" ) !== -1 ) {

			// ...and if the input doesn't have the 'force-down' class and the next across input is available but empty, focus on the next across input...
			if ( inputClass.indexOf( "force-down" ) === -1 && nextAcrossVal === "" ) {
				$( nextAcross ).focus();
			}

			// ...otherwise focus on the next down input and add the force-down class to the next down input.
			else {
				$( nextDown ).addClass( "force-down" ).focus();
			}
		}

		// If the input is ONLY for down responses focus on the next down input and add the force-down class to the next down input.
		else if ( inputClass.indexOf( "across" ) === -1 && inputClass.indexOf( "down" ) !== -1 ) {
			$( nextDown ).addClass( "force-down" ).focus();
		}

		// If the input is ONLY for across responses focus on the next across input.
		else {
			$( nextAcross ).focus();
		}

		// Remove the force-down class once the focus has moved on from the current input.
		$( this ).removeClass( "force-down" );
	}
});

Submitting the Responses

Now that we have all of our markup and interactivity created, we still need to handle the submission of the responses. This isn’t a traditional HTML <form> or a SharePoint form, so we need write a function to submit the responses when the submit button is clicked and include it in the $( document ).ready() function. The first step in this function is to include a check to make sure all responses are filled out (not necessarily correct, just not blank). If they aren’t the script doesn’t submit the responses. Next we display a confirm() message to make sure the user wants to submit the form and didn’t accidentally click the submit link.

$( '#crossword-submit' ).click( function( event ) {

	// If you decide to use a hyperlink instead of a button input, this will prevent the hyperlink from actually navigating away from the page or to an anchor.
	event.preventDefault();

	// Disable the button so the user can't click it again and submit the answers more than once.
	$( this ).prop( 'disabled', true );

	// Prevent submission if the crossword isn't completed.
	if ( $( '#crossword' ).find( 'input' ).filter( function() { return $( this ).val() === ""; }).length !== 0 ) {
		alert( "You have left some answers blank. Please complete all answers before submitting." );
		$( this ).removeProp( 'disabled' );
		return false;
	}

	// Confirm that the user wants to submit their answers.
	var confirmResponse = confirm( "Are you sure you are ready to submit your answers? Once submitted they cannot be changed.\n\nClick OK to continue or Cancel to review your answers." );
	if ( confirmResponse === false ) {
		$( this ).removeProp( 'disabled' );
		return false;
	}

	// Placeholder. We'll do more submit stuff here in a moment
});

Putting the Responses Together

When we created the SharePoint list, we only created a column for each clue, not for each letter in the grid. Before we submit the form, we need to concatenate the letters for each response into a single string of text. Fortunately jQuery makes this easy because we’ve included a class name for each clue response, and jQuery automagically orders those elements based on the DOM tree (which is left-to-right, top-to-bottom just like our crossword responses). For example, if we want to get the letters for 1 Across, we can do so like this:

var OneAcrossAnswer = "";
$( '.one-across' ).each( function() {
	OneAcrossAnswer += $( this ).val().toLowerCase();
});

This would select all <input />s that have the class name “one-across” in left-to-right order and append each letter (in order and lowercase) to the OneAcrossAnswer variable. We could then use this value in an SPServices function to create a new list item. I convert the letters to lowercase so all responses can be checked against a standard answer key without worrying about casing issues.

Making the Script Scale

The previous example would require writing similar code for each and every clue, which isn’t very sustainable for larger crosswords. Fortunately we can put all of our clue response information into a JavaScript object and iterate over each response via jQuery’s $.each() method. Because we’ll be using SPServices’ UpdateListItems operation, we want to build a batch command that contains the responses for all of the columns in the SharePoint list and pass it along to the operation.

First, create a JavaScript object called responses and populate it with a unique name for each of the clues. For each clue, include a selector property for the class name and a column property for the column name. Also create an empty answer property to hold the answer (which we’ll retrieve later). This should be placed right after the confirm() check in the script above.

// Create an object to associate each SharePoint column with the class name used for the input and the user's response for that column
var responses = {
	"oneAcross": {
		"selector": "one-across",
		"column": "OneAcross",
		"answer": ""
	},
	"oneDown": {
		"selector": "one-down",
		"column": "OneDown",
		"answer": ""
	},
	"twoDown": {
		"selector": "two-down",
		"column": "TwoDown",
		"answer": ""
	},
	"threeDown": {
		"selector": "three-down",
		"column": "ThreeDown",
		"answer": ""
	},
	"fourAcross": {
		"selector": "four-across",
		"column": "FourAcross",
		"answer": ""
	}
};

Next create a batchCmd variable to hold the batch command that we’ll be building with our script. It will just contain the start of the batch command for now. We’ll append each response to it and close it later in the script before using SPServices.

// Create the batchCmd variable that will be used to create the new list item
var batchCmd = '<Batch OnError="Continue"><Method ID="1" Cmd="New">';

Now we’re ready to add the responses to the batch command. Using jQuery’s $.each() method, we can iterate over each response (using the response object we created) and add the appropriate information to the batch command using the properties of each response.

// Concatenate values of each response input by iterating over the object representing the responses
$.each( responses, function() {

	// Cache the current item in the responses object.
	var $response = this;

	// For each input in the crossword associated with the current response object, get the values and save them in the answer property.
	$( '.' + $response.selector ).each( function() {
		$response.answer += $( this ).val().toLowerCase();
	});

	// Add the response to the batchCmd
	batchCmd += '<Field Name="' + $response.column + '"><![CDATA[' + $response.answer + ']]></Field>';
});

Creating the List Item

Once the answers have been added, we can finish the batchCmd variable so we have a valid batch command for creating a list item in SharePoint.

// Close the batchCmd variable
batchCmd += '</Method></Batch>';

Now we can use the batchCmd in our SPServices function. Be sure to include the webURL and listName options for your crossword list, which will depend on where you created the list and what you named it. I’ve also included some basic error handling in the SPServices function and a redirect once the answers have been submitted. You can modify the code inside the completefunc function for your specific use-case.

// Create a new list item on the destination list using the batchCmd variable
$().SPServices({
	operation: "UpdateListItems",
	async: true,
	webURL: "http://Server/SiteName",
	listName: "Crossword",
	updates: batchCmd,
	completefunc: function( xData, Status ) {

		// If the AJAX call could not be completed, alert the user or include your own code to handle errors.
		if ( Status !== "success" ) {
			alert( "There was a problem submitting your answers. Please try again later." );
		}
		else {

			// If there was an error creating the list item, alert the user or include your own code to handle errors.
			if ( $( xData.responseXML ).find( 'ErrorCode' ).text() !== "0x00000000" ) {
				alert( "There was a problem submitting your answers. Please try again later." );
			}
			// if the list item was successfully created, alert the user and navigate to the Source parameter in the URL (or to a URL of your choosing).
			else {
				alert( "Your answers were submitted successfully! Click OK to continue." );

				if ( window.location.href.indexOf( "Source=" ) !== -1 ) {
					var url = window.location.href.split( "Source=" )[1].split( "&" )[0];
					window.location.href = url;
				}
				else {
					window.location.href = "/"; // Change this to a default URL to navigate to after the crossword is submitted
				}
			}
		}
	}
});

Here is the full CSS, JavaScript, and HTML that I used in my demo page:

<style type="text/css">
#crossword {
	color: #444;
	font-family: arial, sans-serif;
	font-size: 18px;
}
#crossword td {
	background-color: #fff;
	border: 1px solid #777;
	box-sizing: border-box;
	width: 26px;
	height: 26px;
	vertical-align: middle;
}
#crossword td div {
	position: relative;
	width: 24px;
	height: 24px;
}
#crossword td span {
	display: block;
	position: absolute;
	z-index: 2;
	font-size: .7em;
	line-height: 1em;
	top: 1px;
	left: 1px;
}
#crossword td.empty {
	background-color: #aaa;
}
#crossword td input {
	background-color: #fff;
	border: none;
	color: #777;
	display: block;
	font-family: arial, sans-serif;
	font-size: inherit;
	line-height: 22px;
	width: 22px;
	height: 22px;
	text-align: center;
	text-transform: uppercase;

}
.crossword-heading {
	clear: both;
}
.crossword-questions {
	overflow: hidden;
}
.crossword-questions dt {
	float: left;
}
.crossword-questions dt {
	clear: both;
	width: 2em;
	text-align: right;
}
.crossword-questions dd {
	margin-left: 3em;
}
</style>
<script type="text/javascript">
$( document ).ready( function() {

	// Add interactivity on mouseup and keyup for the inputs
	$( '#crossword' ).find( 'input' ).mouseup( function( event ) {

		// Select the letter in the input so it can be replaced by simply typing another letter
		$( this ).select();
		event.preventDefault();
	}).focus( function() {

		// Select the letter in the input so it can be replaced by simply typing another letter
		$( this ).select();
	}).keyup( function( event ) {

		var code = event.keyCode || event.which;

		// Don't do anything if the Tab, Shift, Backspace, Space, or Delete keys are pressed.
		if ( code === 9 || code === 16 || code === 8 || code === 46 ) {
			return true;
		}

		// Automagically remove spaces when they are typed.
		else if ( code === 32 ) {
			$( this ).val( $( this ).val().replace(/\s/g, ''));
		}

		// Automatically focus on the next appropriate input once a letter or symbol is entered or an arrow key is pressed.
		// This is sort of a best-guess interaction based on where the user is typing.
		else {

			// Cache several variables that will be used to move focus to other inputs in the crossword.
			// If we try to get an input that doesn't exist, the script fails gracefully by simply not moving focus to another input.
			// Cache the class of the current input.
			var inputClass = $( this ).attr( 'class' );

			// Cache the table cell containing the current input.
			var cell = $( this ).closest( 'td' );

			// Cache the index of the current table cell.
			var colIndex = $( cell ).parent().children().index( cell );

			// Cache the next input to the right of the current input.
			var nextAcross = $( cell ).next().find( 'input' );

			// Cache the previous input to the left of the current input.
			var prevAcross = $( cell ).prev().find( 'input' );

			// Cache the value of the next input to the right of the current input.
			var nextAcrossVal = $( nextAcross ).val();

			// Cache the next input below the current input (this is why we need the column index of the current table cell).
			var nextDown = $( cell ).parent().next().find( 'td:eq(' + colIndex + ')' ).find( 'input' );

			// Cache the previous input above the current input (this is why we need the column index of the current table cell).
			var prevDown = $( cell ).parent().prev().find( 'td:eq(' + colIndex + ')' ).find( 'input' );

			// If the arrow keys are pressed, go to the appropriate input (if available).
			switch ( code ) {
				case 37: // left arrow
					prevAcross.focus();
					return false;
					break;
				case 38: // up arrow
					prevDown.focus();
					return false;
					break;
				case 39: // right arrow
					nextAcross.focus();
					return false;
					break;
				case 40: // down arrow
					nextDown.focus().addClass( "force-down" ); // If user pressed the down arrow, they probably want to continue typing a down response.
					return false;
					break;
				default:
			}

			// If the input is for both across AND down responses...
			if ( inputClass.indexOf( "across" ) !== -1 && inputClass.indexOf( "down" ) !== -1 ) {

				// ...and if the input doesn't have the 'force-down' class and the next across input is available but empty, focus on the next across input...
				if ( inputClass.indexOf( "force-down" ) === -1 && nextAcrossVal === "" ) {
					$( nextAcross ).focus();
				}

				// ...otherwise focus on the next down input and add the force-down class to the next down input.
				else {
					$( nextDown ).addClass( "force-down" ).focus();
				}
			}

			// If the input is ONLY for down responses focus on the next down input and add the force-down class to the next down input.
			else if ( inputClass.indexOf( "across" ) === -1 && inputClass.indexOf( "down" ) !== -1 ) {
				$( nextDown ).addClass( "force-down" ).focus();
			}

			// If the input is ONLY for across responses focus on the next across input.
			else {
				$( nextAcross ).focus();
			}

			// Remove the force-down class once the focus has moved on from the current input.
			$( this ).removeClass( "force-down" );
		}
	});

	$( '#crossword-submit' ).click( function( event ) {

		// If you decide to use a hyperlink instead of a button input, this will prevent the hyperlink from actually navigating away from the page or to an anchor.
		event.preventDefault();

		// Disable the button so the user can't click it again and submit the answers more than once.
		$( this ).prop( 'disabled', true );

		// Prevent submission if the crossword isn't completed.
		if ( $( '#crossword' ).find( 'input' ).filter( function() { return $( this ).val() === ""; }).length !== 0 ) {
			alert( "You have left some answers blank. Please complete all answers before submitting." );
			$( this ).removeProp( 'disabled' );
			return false;
		}

		// Confirm that the user wants to submit their answers.
		var confirmResponse = confirm( "Are you sure you are ready to submit your answers? Once submitted they cannot be changed.\n\nClick OK to continue or Cancel to review your answers." );
		if ( confirmResponse === false ) {
			$( this ).removeProp( 'disabled' );
			return false;
		}

		// Create an object to associate each SharePoint column with the class name used for the input and the user's response for that column
		var responses = {
			"oneAcross": {
				"selector": "one-across",
				"column": "OneAcross",
				"answer": ""
			},
			"oneDown": {
				"selector": "one-down",
				"column": "OneDown",
				"answer": ""
			},
			"twoDown": {
				"selector": "two-down",
				"column": "TwoDown",
				"answer": ""
			},
			"threeDown": {
				"selector": "three-down",
				"column": "ThreeDown",
				"answer": ""
			},
			"fourAcross": {
				"selector": "four-across",
				"column": "FourAcross",
				"answer": ""
			}
		};

		// Create the batchCmd variable that will be used to create the new list item
		var batchCmd = '<Batch OnError="Continue"><Method ID="1" Cmd="New">';

		// Concatenate values of each response input by iterating over the object representing the responses
		$.each( responses, function() {

			// Cache the current item in the responses object.
			var $response = this;

			// For each input in the crossword associated with the current response object, get the values and save them in the answer property.
			$( '.' + $response.selector ).each( function() {
				$response.answer += $( this ).val().toLowerCase();
			});

			// Add the response to the batchCmd
			batchCmd += '<Field Name="' + $response.column + '"><![CDATA[' + $response.answer + ']]></Field>';
		});

		// Close the batchCmd variable
		batchCmd += '</Method></Batch>';

		// Create a new list item on the destination list using the batchCmd variable
		$().SPServices({
			operation: "UpdateListItems",
			async: true,
			webURL: "http://Server/SiteName",
			listName: "Crossword",
			updates: batchCmd,
			completefunc: function( xData, Status ) {

				// If the AJAX call could not be completed, alert the user or include your own code to handle errors.
				if ( Status !== "success" ) {
					alert( "There was a problem submitting your answers. Please try again later." );
				}
				else {

					// If there was an error creating the list item, alert the user or include your own code to handle errors.
					if ( $( xData.responseXML ).find( 'ErrorCode' ).text() !== "0x00000000" ) {
						alert( "There was a problem submitting your answers. Please try again later." );
					}
					// if the list item was successfully created, alert the user and navigate to the Source parameter in the URL (or to a URL of your choosing).
					else {
						alert( "Your answers were submitted successfully! Click OK to continue." );

						if ( window.location.href.indexOf( "Source=" ) !== -1 ) {
							var url = window.location.href.split( "Source=" )[1].split( "&" )[0];
							window.location.href = url;
						}
						else {
							window.location.href = "/";
						}
					}
				}
			}
		});
	});
});
</script>
<table id="crossword" border="0" cellpadding="0" cellspacing="0">
	<tbody>
		<tr>
			<td><div><span>1</span><input type="text" maxlength="1" class="one-across one-down" /></div></td>
			<td><div><input type="text" maxlength="1" class="one-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="one-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="one-across" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><span>2</span><input type="text" maxlength="1" class="two-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="two-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="two-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="two-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><span>3</span><input type="text" maxlength="1" class="three-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><span>4</span><input type="text" maxlength="1" class="one-down four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="two-down four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="three-down four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
			<td><div><input type="text" maxlength="1" class="four-across" /></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="two-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="three-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="three-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td><div><input type="text" maxlength="1" class="one-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="three-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
		<tr>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
			<td><div><input type="text" maxlength="1" class="three-down" /></div></td>
			<td class="empty"><div></div></td>
			<td class="empty"><div></div></td>
		</tr>
	</tbody>
</table>

<p><input type="button" id="crossword-submit" value="Submit Your Crossword" /></p>

<h4 class="crossword-heading">Across</h4>
<dl class="crossword-questions">
	<dt>1.</dt>
	<dd>Abbreviation for the commercial version of SharePoint 2007.</dd>

	<dt>4.</dt>
	<dd>A JavaScript library built for interacting with SharePoint Web Services.</dd>
</dl>

<h4 class="crossword-heading">Down</h4>
<dl class="crossword-questions">
	<dt>1.</dt>
	<dd>The company that created SharePoint.</dd>

	<dt>2.</dt>
	<dd>“Write less, do more” with this popular JavaScript library.</dd>

	<dt>3.</dt>
	<dd>JavaScript can be added to a page using a ______ element.</dd>
</dl>

Other Considerations

SharePoint has limits on the total number of columns that can be used in a custom list, which means you can’t have a crossword puzzle that has more clues than your list can have columns. The performance of this example is also dependent on the user’s browser and computer hardware. I’ve noticed that if I type a response too fast, the code that handles moving focus to the next <input /> can’t quite keep up and I’ll end up with a blank <input /> or two. You could always remove the code that changes the focus and let users simply navigate using the Tab key or their mouse to avoid this, or optimize the script further (perhaps bind to keydown instead of keyup).

I think SharePoint can make a decent platform for simple games like this with a little creativity. Let me know if you have any suggestions/questions or if you’ve seen other game implementations using SharePoint in the comments!


Comments

  1. Original and a very interesting post!
  2. excellent. liking the stuff on your site. nice to run across a fellow sharepoint + web designer.
  3. Nice one, its really super man. carry on!!!
  4. You are my savior! This stuff is amazing and I am gonna implement it. Thank you!
  5. Hello is this solution also works with SharePoint 2010? I tried it but was unable to get the table

Comments are closed