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