PageSourceSearch

https://stackernewscrosswordpuzzle2.vercel.app/js/jquery.crossword.js

js stackernewscrosswordpuzzle2.vercel.app collected 2026-10-03 07:47:25 UTC 17,707 bytes, 605 lines download raw bytes

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.