├── README
├── index.html
└── js
├── jquery.crossword.js
└── script.js
/README:
--------------------------------------------------------------------------------
1 | I built this crossword puzzle to provide an enhanced, more intuitive user experience with javascript.
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Qurossword - A javascript crossword puzzle plugin
10 |
11 |
12 |
13 |
14 |
15 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/js/jquery.crossword.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Jesse Weisbeck's Crossword Puzzle (for all 3 people left who want to play them)
4 | *
5 | */
6 | (function($){
7 | $.fn.crossword = function(entryData) {
8 | /*
9 | Qurossword Puzzle: a javascript + jQuery crossword puzzle
10 | "light" refers to a white box - or an input
11 |
12 | DEV NOTES:
13 | - activePosition and activeClueIndex are the primary vars that set the ui whenever there's an interaction
14 | - 'Entry' is a puzzler term used to describe the group of letter inputs representing a word solution
15 | - This puzzle isn't designed to securely hide answerers. A user can see answerers in the js source
16 | - An xhr provision can be added later to hit an endpoint on keyup to check the answerer
17 | - The ordering of the array of problems doesn't matter. The position & orientation properties is enough information
18 | - Puzzle authors must provide a starting x,y coordinates for each entry
19 | - Entry orientation must be provided in lieu of provided ending x,y coordinates (script could be adjust to use ending x,y coords)
20 | - Answers are best provided in lower-case, and can NOT have spaces - will add support for that later
21 | */
22 |
23 | var puzz = {}; // put data array in object literal to namespace it into safety
24 | puzz.data = entryData;
25 |
26 | // append clues markup after puzzle wrapper div
27 | // This should be moved into a configuration object
28 | this.after('');
29 |
30 | // initialize some variables
31 | var tbl = [''],
32 | puzzEl = this,
33 | clues = $('#puzzle-clues'),
34 | clueLiEls,
35 | coords,
36 | entryCount = puzz.data.length,
37 | entries = [],
38 | rows = [],
39 | cols = [],
40 | solved = [],
41 | tabindex,
42 | $actives,
43 | activePosition = 0,
44 | activeClueIndex = 0,
45 | currOri,
46 | targetInput,
47 | mode = 'interacting',
48 | solvedToggle = false,
49 | z = 0;
50 |
51 | var puzInit = {
52 |
53 | init: function() {
54 | currOri = 'across'; // app's init orientation could move to config object
55 |
56 | // Reorder the problems array ascending by POSITION
57 | puzz.data.sort(function(a,b) {
58 | return a.position - b.position;
59 | });
60 |
61 | // Set keyup handlers for the 'entry' inputs that will be added presently
62 | puzzEl.delegate('input', 'keyup', function(e){
63 | mode = 'interacting';
64 |
65 |
66 | // need to figure out orientation up front, before we attempt to highlight an entry
67 | switch(e.which) {
68 | case 39:
69 | case 37:
70 | currOri = 'across';
71 | break;
72 | case 38:
73 | case 40:
74 | currOri = 'down';
75 | break;
76 | default:
77 | break;
78 | }
79 |
80 | if ( e.keyCode === 9) {
81 | return false;
82 | } else if (
83 | e.keyCode === 37 ||
84 | e.keyCode === 38 ||
85 | e.keyCode === 39 ||
86 | e.keyCode === 40 ||
87 | e.keyCode === 8 ||
88 | e.keyCode === 46 ) {
89 |
90 |
91 |
92 | if (e.keyCode === 8 || e.keyCode === 46) {
93 | currOri === 'across' ? nav.nextPrevNav(e, 37) : nav.nextPrevNav(e, 38);
94 | } else {
95 | nav.nextPrevNav(e);
96 | }
97 |
98 | e.preventDefault();
99 | return false;
100 | } else {
101 |
102 | console.log('input keyup: '+solvedToggle);
103 |
104 | puzInit.checkAnswer(e);
105 |
106 | }
107 |
108 | e.preventDefault();
109 | return false;
110 | });
111 |
112 | // tab navigation handler setup
113 | puzzEl.delegate('input', 'keydown', function(e) {
114 |
115 | if ( e.keyCode === 9) {
116 |
117 | mode = "setting ui";
118 | if (solvedToggle) solvedToggle = false;
119 |
120 | //puzInit.checkAnswer(e)
121 | nav.updateByEntry(e);
122 |
123 | } else {
124 | return true;
125 | }
126 |
127 | e.preventDefault();
128 |
129 | });
130 |
131 | // tab navigation handler setup
132 | puzzEl.delegate('input', 'click', function(e) {
133 | mode = "setting ui";
134 | if (solvedToggle) solvedToggle = false;
135 |
136 | console.log('input click: '+solvedToggle);
137 |
138 | nav.updateByEntry(e);
139 | e.preventDefault();
140 |
141 | });
142 |
143 |
144 | // click/tab clues 'navigation' handler setup
145 | clues.delegate('li', 'click', function(e) {
146 | mode = 'setting ui';
147 |
148 | if (!e.keyCode) {
149 | nav.updateByNav(e);
150 | }
151 | e.preventDefault();
152 | });
153 |
154 |
155 | // highlight the letter in selected 'light' - better ux than making user highlight letter with second action
156 | puzzEl.delegate('#puzzle', 'click', function(e) {
157 | $(e.target).focus();
158 | $(e.target).select();
159 | });
160 |
161 | // DELETE FOR BG
162 | puzInit.calcCoords();
163 |
164 | // Puzzle clues added to DOM in calcCoords(), so now immediately put mouse focus on first clue
165 | clueLiEls = $('#puzzle-clues li');
166 | $('#' + currOri + ' li' ).eq(0).addClass('clues-active').focus();
167 |
168 | // DELETE FOR BG
169 | puzInit.buildTable();
170 | puzInit.buildEntries();
171 |
172 | },
173 |
174 | /*
175 | - Given beginning coordinates, calculate all coordinates for entries, puts them into entries array
176 | - Builds clue markup and puts screen focus on the first one
177 | */
178 | calcCoords: function() {
179 | /*
180 | Calculate all puzzle entry coordinates, put into entries array
181 | */
182 | for (var i = 0, p = entryCount; i < p; ++i) {
183 | // set up array of coordinates for each problem
184 | entries.push(i);
185 | entries[i] = [];
186 |
187 | for (var x=0, j = puzz.data[i].answer.length; x < j; ++x) {
188 | entries[i].push(x);
189 | coords = puzz.data[i].orientation === 'across' ? "" + puzz.data[i].startx++ + "," + puzz.data[i].starty + "" : "" + puzz.data[i].startx + "," + puzz.data[i].starty++ + "" ;
190 | entries[i][x] = coords;
191 | }
192 |
193 | // while we're in here, add clues to DOM!
194 | $('#' + puzz.data[i].orientation).append('' + puzz.data[i].clue + ' ');
195 | }
196 |
197 | // Calculate rows/cols by finding max coords of each entry, then picking the highest
198 | for (var i = 0, p = entryCount; i < p; ++i) {
199 | for (var x=0; x < entries[i].length; x++) {
200 | cols.push(entries[i][x].split(',')[0]);
201 | rows.push(entries[i][x].split(',')[1]);
202 | };
203 | }
204 |
205 | rows = Math.max.apply(Math, rows) + "";
206 | cols = Math.max.apply(Math, cols) + "";
207 |
208 | },
209 |
210 | /*
211 | Build the table markup
212 | - adds [data-coords] to each cell
213 | */
214 | buildTable: function() {
215 | for (var i=1; i <= rows; ++i) {
216 | tbl.push(" ");
217 | for (var x=1; x <= cols; ++x) {
218 | tbl.push(' ');
219 | };
220 | tbl.push(" ");
221 | };
222 |
223 | tbl.push("
");
224 | puzzEl.append(tbl.join(''));
225 | },
226 |
227 | /*
228 | Builds entries into table
229 | - Adds entry class(es) to cells
230 | - Adds tabindexes to
231 | */
232 | buildEntries: function() {
233 | var puzzCells = $('#puzzle td'),
234 | light,
235 | $groupedLights,
236 | hasOffset = false,
237 | positionOffset = entryCount - puzz.data[puzz.data.length-1].position; // diff. between total ENTRIES and highest POSITIONS
238 |
239 | for (var x=1, p = entryCount; x <= p; ++x) {
240 | var letters = puzz.data[x-1].answer.split('');
241 |
242 | for (var i=0; i < entries[x-1].length; ++i) {
243 | light = $(puzzCells +'[data-coords="' + entries[x-1][i] + '"]');
244 |
245 | // check if POSITION property of the entry on current go-round is same as previous.
246 | // If so, it means there's an across & down entry for the position.
247 | // Therefore you need to subtract the offset when applying the entry class.
248 | if(x > 1 ){
249 | if (puzz.data[x-1].position === puzz.data[x-2].position) {
250 | hasOffset = true;
251 | };
252 | }
253 |
254 | if($(light).empty()){
255 | $(light)
256 | .addClass('entry-' + (hasOffset ? x - positionOffset : x) + ' position-' + (x-1) )
257 | .append(' ');
258 | }
259 | };
260 |
261 | };
262 |
263 | // Put entry number in first 'light' of each entry, skipping it if already present
264 | for (var i=1, p = entryCount; i < p; ++i) {
265 | $groupedLights = $('.entry-' + i);
266 | if(!$('.entry-' + i +':eq(0) span').length){
267 | $groupedLights.eq(0)
268 | .append('' + puzz.data[i].position + ' ');
269 | }
270 | }
271 |
272 | util.highlightEntry();
273 | util.highlightClue();
274 | $('.active').eq(0).focus();
275 | $('.active').eq(0).select();
276 |
277 | },
278 |
279 |
280 | /*
281 | - Checks current entry input group value against answer
282 | - If not complete, auto-selects next input for user
283 | */
284 | checkAnswer: function(e) {
285 |
286 | var valToCheck, currVal;
287 |
288 | util.getActivePositionFromClassGroup($(e.target));
289 |
290 | valToCheck = puzz.data[activePosition].answer.toLowerCase();
291 |
292 | currVal = $('.position-' + activePosition + ' input')
293 | .map(function() {
294 | return $(this)
295 | .val()
296 | .toLowerCase();
297 | })
298 | .get()
299 | .join('');
300 |
301 | //console.log(currVal + " " + valToCheck);
302 | if(valToCheck === currVal){
303 | $('.active')
304 | .addClass('done')
305 | .removeClass('active');
306 |
307 | $('.clues-active').addClass('clue-done');
308 |
309 | solved.push(valToCheck);
310 | solvedToggle = true;
311 | return;
312 | }
313 |
314 | currOri === 'across' ? nav.nextPrevNav(e, 39) : nav.nextPrevNav(e, 40);
315 |
316 | //z++;
317 | //console.log(z);
318 | //console.log('checkAnswer() solvedToggle: '+solvedToggle);
319 |
320 | }
321 |
322 |
323 | }; // end puzInit object
324 |
325 |
326 | var nav = {
327 |
328 | nextPrevNav: function(e, override) {
329 |
330 | var len = $actives.length,
331 | struck = override ? override : e.which,
332 | el = $(e.target),
333 | p = el.parent(),
334 | ps = el.parents(),
335 | selector;
336 |
337 | util.getActivePositionFromClassGroup(el);
338 | util.highlightEntry();
339 | util.highlightClue();
340 |
341 | $('.current').removeClass('current');
342 |
343 | selector = '.position-' + activePosition + ' input';
344 |
345 | //console.log('nextPrevNav activePosition & struck: '+ activePosition + ' '+struck);
346 |
347 | // move input focus/select to 'next' input
348 | switch(struck) {
349 | case 39:
350 | p
351 | .next()
352 | .find('input')
353 | .addClass('current')
354 | .select();
355 |
356 | break;
357 |
358 | case 37:
359 | p
360 | .prev()
361 | .find('input')
362 | .addClass('current')
363 | .select();
364 |
365 | break;
366 |
367 | case 40:
368 | ps
369 | .next('tr')
370 | .find(selector)
371 | .addClass('current')
372 | .select();
373 |
374 | break;
375 |
376 | case 38:
377 | ps
378 | .prev('tr')
379 | .find(selector)
380 | .addClass('current')
381 | .select();
382 |
383 | break;
384 |
385 | default:
386 | break;
387 | }
388 |
389 | },
390 |
391 | updateByNav: function(e) {
392 | var target;
393 |
394 | $('.clues-active').removeClass('clues-active');
395 | $('.active').removeClass('active');
396 | $('.current').removeClass('current');
397 | currIndex = 0;
398 |
399 | target = e.target;
400 | activePosition = $(e.target).data('position');
401 |
402 | util.highlightEntry();
403 | util.highlightClue();
404 |
405 | $('.active').eq(0).focus();
406 | $('.active').eq(0).select();
407 | $('.active').eq(0).addClass('current');
408 |
409 | // store orientation for 'smart' auto-selecting next input
410 | currOri = $('.clues-active').parent('ol').prop('id');
411 |
412 | activeClueIndex = $(clueLiEls).index(e.target);
413 | //console.log('updateByNav() activeClueIndex: '+activeClueIndex);
414 |
415 | },
416 |
417 | // Sets activePosition var and adds active class to current entry
418 | updateByEntry: function(e, next) {
419 | var classes, next, clue, e1Ori, e2Ori, e1Cell, e2Cell;
420 |
421 | if(e.keyCode === 9 || next){
422 | // handle tabbing through problems, which keys off clues and requires different handling
423 | activeClueIndex = activeClueIndex === clueLiEls.length-1 ? 0 : ++activeClueIndex;
424 |
425 | $('.clues-active').removeClass('.clues-active');
426 |
427 | next = $(clueLiEls[activeClueIndex]);
428 | currOri = next.parent().prop('id');
429 | activePosition = $(next).data('position');
430 |
431 | // skips over already-solved problems
432 | util.getSkips(activeClueIndex);
433 | activePosition = $(clueLiEls[activeClueIndex]).data('position');
434 |
435 |
436 | } else {
437 | activeClueIndex = activeClueIndex === clueLiEls.length-1 ? 0 : ++activeClueIndex;
438 |
439 | util.getActivePositionFromClassGroup(e.target);
440 |
441 | clue = $(clueLiEls + '[data-position=' + activePosition + ']');
442 | activeClueIndex = $(clueLiEls).index(clue);
443 |
444 | currOri = clue.parent().prop('id');
445 |
446 | }
447 |
448 | util.highlightEntry();
449 | util.highlightClue();
450 |
451 | //$actives.eq(0).addClass('current');
452 | //console.log('nav.updateByEntry() reports activePosition as: '+activePosition);
453 | }
454 |
455 | }; // end nav object
456 |
457 |
458 | var util = {
459 | highlightEntry: function() {
460 | // this routine needs to be smarter because it doesn't need to fire every time, only
461 | // when activePosition changes
462 | $actives = $('.active');
463 | $actives.removeClass('active');
464 | $actives = $('.position-' + activePosition + ' input').addClass('active');
465 | $actives.eq(0).focus();
466 | $actives.eq(0).select();
467 | },
468 |
469 | highlightClue: function() {
470 | var clue;
471 | $('.clues-active').removeClass('clues-active');
472 | $(clueLiEls + '[data-position=' + activePosition + ']').addClass('clues-active');
473 |
474 | if (mode === 'interacting') {
475 | clue = $(clueLiEls + '[data-position=' + activePosition + ']');
476 | activeClueIndex = $(clueLiEls).index(clue);
477 | };
478 | },
479 |
480 | getClasses: function(light, type) {
481 | if (!light.length) return false;
482 |
483 | var classes = $(light).prop('class').split(' '),
484 | classLen = classes.length,
485 | positions = [];
486 |
487 | // pluck out just the position classes
488 | for(var i=0; i < classLen; ++i){
489 | if (!classes[i].indexOf(type) ) {
490 | positions.push(classes[i]);
491 | }
492 | }
493 |
494 | return positions;
495 | },
496 |
497 | getActivePositionFromClassGroup: function(el){
498 |
499 | classes = util.getClasses($(el).parent(), 'position');
500 |
501 | if(classes.length > 1){
502 | // get orientation for each reported position
503 | e1Ori = $(clueLiEls + '[data-position=' + classes[0].split('-')[1] + ']').parent().prop('id');
504 | e2Ori = $(clueLiEls + '[data-position=' + classes[1].split('-')[1] + ']').parent().prop('id');
505 |
506 | // test if clicked input is first in series. If so, and it intersects with
507 | // entry of opposite orientation, switch to select this one instead
508 | e1Cell = $('.position-' + classes[0].split('-')[1] + ' input').index(el);
509 | e2Cell = $('.position-' + classes[1].split('-')[1] + ' input').index(el);
510 |
511 | if(mode === "setting ui"){
512 | currOri = e1Cell === 0 ? e1Ori : e2Ori; // change orientation if cell clicked was first in a entry of opposite direction
513 | }
514 |
515 | if(e1Ori === currOri){
516 | activePosition = classes[0].split('-')[1];
517 | } else if(e2Ori === currOri){
518 | activePosition = classes[1].split('-')[1];
519 | }
520 | } else {
521 | activePosition = classes[0].split('-')[1];
522 | }
523 |
524 | console.log('getActivePositionFromClassGroup activePosition: '+activePosition);
525 |
526 | },
527 |
528 | checkSolved: function(valToCheck) {
529 | for (var i=0, s=solved.length; i < s; i++) {
530 | if(valToCheck === solved[i]){
531 | return true;
532 | }
533 |
534 | }
535 | },
536 |
537 | getSkips: function(position) {
538 | if ($(clueLiEls[position]).hasClass('clue-done')){
539 | activeClueIndex = position === clueLiEls.length-1 ? 0 : ++activeClueIndex;
540 | util.getSkips(activeClueIndex);
541 | } else {
542 | return false;
543 | }
544 | }
545 |
546 | }; // end util object
547 |
548 |
549 | puzInit.init();
550 |
551 |
552 | }
553 |
554 | })(jQuery);
--------------------------------------------------------------------------------
/js/script.js:
--------------------------------------------------------------------------------
1 | // A javascript-enhanced crossword puzzle [c] Jesse Weisbeck, MIT/GPL
2 | (function($) {
3 | $(function() {
4 | // provide crossword entries in an array of objects like the following example
5 | // Position refers to the numerical order of an entry. Each position can have
6 | // two entries: an across entry and a down entry
7 | var puzzleData = [
8 | {
9 | clue: "First letter of greek alphabet",
10 | answer: "alpha",
11 | position: 1,
12 | orientation: "across",
13 | startx: 1,
14 | starty: 1
15 | },
16 | {
17 | clue: "Not a one ___ motor, but a three ___ motor",
18 | answer: "phase",
19 | position: 3,
20 | orientation: "across",
21 | startx: 7,
22 | starty: 1
23 | },
24 | {
25 | clue: "Created from a separation of charge",
26 | answer: "capacitance",
27 | position: 5,
28 | orientation: "across",
29 | startx: 1,
30 | starty: 3
31 | },
32 | {
33 | clue: "The speeds of engines without and accelaration",
34 | answer: "idlespeeds",
35 | position: 8,
36 | orientation: "across",
37 | startx: 1,
38 | starty: 5
39 | },
40 | {
41 | clue: "Complex resistances",
42 | answer: "impedances",
43 | position: 10,
44 | orientation: "across",
45 | startx: 2,
46 | starty: 7
47 | },
48 | {
49 | clue: "This device is used to step-up, step-down, and/or isolate",
50 | answer: "transformer",
51 | position: 13,
52 | orientation: "across",
53 | startx: 1,
54 | starty: 9
55 | },
56 | {
57 | clue: "Type of ray emitted frm the sun",
58 | answer: "gamma",
59 | position: 16,
60 | orientation: "across",
61 | startx: 1,
62 | starty: 11
63 | },
64 | {
65 | clue: "C programming language operator",
66 | answer: "cysan",
67 | position: 17,
68 | orientation: "across",
69 | startx: 7,
70 | starty: 11
71 | },
72 | {
73 | clue: "Defines the alpha-numeric characters that are typically associated with text used in programming",
74 | answer: "ascii",
75 | position: 1,
76 | orientation: "down",
77 | startx: 1,
78 | starty: 1
79 | },
80 | {
81 | clue: "Generally, if you go over 1kV per cm this happens",
82 | answer: "arc",
83 | position: 2,
84 | orientation: "down",
85 | startx: 5,
86 | starty: 1
87 | },
88 | {
89 | clue: "Control system strategy that tries to replicate the human through process (abbr.)",
90 | answer: "ann",
91 | position: 4,
92 | orientation: "down",
93 | startx: 9,
94 | starty: 1
95 | },
96 | {
97 | clue: "Greek variable that usually describes rotor positon",
98 | answer: "theta",
99 | position: 6,
100 | orientation: "down",
101 | startx: 7,
102 | starty: 3
103 | },
104 | {
105 | clue: "Electromagnetic (abbr.)",
106 | answer: "em",
107 | position: 7,
108 | orientation: "down",
109 | startx: 11,
110 | starty: 3
111 | },
112 | {
113 | clue: "No. 13 across does this to a voltage",
114 | answer: "steps",
115 | position: 9,
116 | orientation: "down",
117 | startx: 5,
118 | starty: 5
119 | },
120 | {
121 | clue: "Emits a lout wailing sound",
122 | answer: "siren",
123 | position: 11,
124 | orientation: "down",
125 | startx: 11,
126 | starty: 7
127 | },
128 | {
129 | clue: "Information technology (abbr.)",
130 | answer: "it",
131 | position: 12,
132 | orientation: "down",
133 | startx: 1,
134 | starty: 8
135 | },
136 | {
137 | clue: "Asynchronous transfer mode (abbr.)",
138 | answer: "atm",
139 | position: 14,
140 | orientation: "down",
141 | startx: 3,
142 | starty: 9
143 | },
144 | {
145 | clue: "Offset current control (abbr.)",
146 | answer: "occ",
147 | position: 15,
148 | orientation: "down",
149 | startx: 7,
150 | starty: 9
151 | }
152 | ]
153 |
154 | $('#puzzle-wrapper').crossword(puzzleData);
155 |
156 | })
157 |
158 | })(jQuery)
159 |
--------------------------------------------------------------------------------