├── 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('

Across

    Down

      '); 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('
    1. ' + puzz.data[i].clue + '
    2. '); 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 "); 217 | for (var x=1; x <= cols; ++x) { 218 | tbl.push(''); 219 | }; 220 | tbl.push(""); 221 | }; 222 | 223 | tbl.push("
      cell 213 | */ 214 | buildTable: function() { 215 | for (var i=1; i <= rows; ++i) { 216 | 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 | --------------------------------------------------------------------------------