Skip to content

Instantly share code, notes, and snippets.

@zismailov
Created July 25, 2017 12:26
Show Gist options
  • Save zismailov/4f60e73e7c2eb7b60f832b13bcb97580 to your computer and use it in GitHub Desktop.
Save zismailov/4f60e73e7c2eb7b60f832b13bcb97580 to your computer and use it in GitHub Desktop.
/**
* Jesse Weisbeck's Crossword Puzzle (for all 3 people left who want to play them)
*
*/
(function($){
$.fn.crossword = function(entryData) {
/*
Qurossword Puzzle: a javascript + jQuery crossword puzzle
"light" refers to a white box - or an input
DEV NOTES:
- activePosition and activeClueIndex are the primary vars that set the ui whenever there's an interaction
- 'Entry' is a puzzler term used to describe the group of letter inputs representing a word solution
- This puzzle isn't designed to securely hide answerers. A user can see answerers in the js source
- An xhr provision can be added later to hit an endpoint on keyup to check the answerer
- The ordering of the array of problems doesn't matter. The position & orientation properties is enough information
- Puzzle authors must provide a starting x,y coordinates for each entry
- Entry orientation must be provided in lieu of provided ending x,y coordinates (script could be adjust to use ending x,y coords)
- Answers are best provided in lower-case, and can NOT have spaces - will add support for that later
*/
var puzz = {}; // put data array in object literal to namespace it into safety
puzz.data = entryData;
// append clues markup after puzzle wrapper div
// This should be moved into a configuration object
this.after('<div id="puzzle-clues"><h2>Across</h2><ol id="across"></ol><h2>Down</h2><ol id="down"></ol></div>');
// initialize some variables
var tbl = ['<table id="puzzle">'],
puzzEl = this,
clues = $('#puzzle-clues'),
clueLiEls,
coords,
entryCount = puzz.data.length,
entries = [],
rows = [],
cols = [],
solved = [],
tabindex,
$actives,
activePosition = 0,
activeClueIndex = 0,
currOri,
targetInput,
mode = 'interacting',
solvedToggle = false,
z = 0;
var puzInit = {
init: function() {
currOri = 'across'; // app's init orientation could move to config object
// Reorder the problems array ascending by POSITION
puzz.data.sort(function(a,b) {
return a.position - b.position;
});
// Set keyup handlers for the 'entry' inputs that will be added presently
puzzEl.delegate('input', 'keyup', function(e){
mode = 'interacting';
// need to figure out orientation up front, before we attempt to highlight an entry
switch(e.which) {
case 39:
case 37:
currOri = 'across';
break;
case 38:
case 40:
currOri = 'down';
break;
default:
break;
}
if ( e.keyCode === 9) {
return false;
} else if (
e.keyCode === 37 ||
e.keyCode === 38 ||
e.keyCode === 39 ||
e.keyCode === 40 ||
e.keyCode === 8 ||
e.keyCode === 46 ) {
if (e.keyCode === 8 || e.keyCode === 46) {
currOri === 'across' ? nav.nextPrevNav(e, 37) : nav.nextPrevNav(e, 38);
} else {
nav.nextPrevNav(e);
}
e.preventDefault();
return false;
} else {
console.log('input keyup: '+solvedToggle);
puzInit.checkAnswer(e);
}
e.preventDefault();
return false;
});
// tab navigation handler setup
puzzEl.delegate('input', 'keydown', function(e) {
if ( e.keyCode === 9) {
mode = "setting ui";
if (solvedToggle) solvedToggle = false;
//puzInit.checkAnswer(e)
nav.updateByEntry(e);
} else {
return true;
}
e.preventDefault();
});
// tab navigation handler setup
puzzEl.delegate('input', 'click', function(e) {
mode = "setting ui";
if (solvedToggle) solvedToggle = false;
console.log('input click: '+solvedToggle);
nav.updateByEntry(e);
e.preventDefault();
});
// click/tab clues 'navigation' handler setup
clues.delegate('li', 'click', function(e) {
mode = 'setting ui';
if (!e.keyCode) {
nav.updateByNav(e);
}
e.preventDefault();
});
// highlight the letter in selected 'light' - better ux than making user highlight letter with second action
puzzEl.delegate('#puzzle', 'click', function(e) {
$(e.target).focus();
$(e.target).select();
});
// DELETE FOR BG
puzInit.calcCoords();
// Puzzle clues added to DOM in calcCoords(), so now immediately put mouse focus on first clue
clueLiEls = $('#puzzle-clues li');
$('#' + currOri + ' li' ).eq(0).addClass('clues-active').focus();
// DELETE FOR BG
puzInit.buildTable();
puzInit.buildEntries();
},
/*
- Given beginning coordinates, calculate all coordinates for entries, puts them into entries array
- Builds clue markup and puts screen focus on the first one
*/
calcCoords: function() {
/*
Calculate all puzzle entry coordinates, put into entries array
*/
for (var i = 0, p = entryCount; i < p; ++i) {
// set up array of coordinates for each problem
entries.push(i);
entries[i] = [];
for (var x=0, j = puzz.data[i].answer.length; x < j; ++x) {
entries[i].push(x);
coords = puzz.data[i].orientation === 'across' ? "" + puzz.data[i].startx++ + "," + puzz.data[i].starty + "" : "" + puzz.data[i].startx + "," + puzz.data[i].starty++ + "" ;
entries[i][x] = coords;
}
// while we're in here, add clues to DOM!
$('#' + puzz.data[i].orientation).append('<li tabindex="1" data-position="' + i + '">' + puzz.data[i].clue + '</li>');
}
// Calculate rows/cols by finding max coords of each entry, then picking the highest
for (var i = 0, p = entryCount; i < p; ++i) {
for (var x=0; x < entries[i].length; x++) {
cols.push(entries[i][x].split(',')[0]);
rows.push(entries[i][x].split(',')[1]);
};
}
rows = Math.max.apply(Math, rows) + "";
cols = Math.max.apply(Math, cols) + "";
},
/*
Build the table markup
- adds [data-coords] to each <td> cell
*/
buildTable: function() {
for (var i=1; i <= rows; ++i) {
tbl.push("<tr>");
for (var x=1; x <= cols; ++x) {
tbl.push('<td data-coords="' + x + ',' + i + '"></td>');
};
tbl.push("</tr>");
};
tbl.push("</table>");
puzzEl.append(tbl.join(''));
},
/*
Builds entries into table
- Adds entry class(es) to <td> cells
- Adds tabindexes to <inputs>
*/
buildEntries: function() {
var puzzCells = $('#puzzle td'),
light,
$groupedLights,
hasOffset = false,
positionOffset = entryCount - puzz.data[puzz.data.length-1].position; // diff. between total ENTRIES and highest POSITIONS
for (var x=1, p = entryCount; x <= p; ++x) {
var letters = puzz.data[x-1].answer.split('');
for (var i=0; i < entries[x-1].length; ++i) {
light =$('#puzzle td[data-coords="' + entries[x-1][i] + '"]');
// check if POSITION property of the entry on current go-round is same as previous.
// If so, it means there's an across & down entry for the position.
// Therefore you need to subtract the offset when applying the entry class.
if(x > 1 ){
if (puzz.data[x-1].position === puzz.data[x-2].position) {
hasOffset = true;
};
}
if($(light).empty()){
$(light)
.addClass('entry-' + (hasOffset ? x - positionOffset : x) + ' position-' + (x-1) )
.append('<input maxlength="1" val="" type="text" tabindex="-1" />');
}
};
};
// Put entry number in first 'light' of each entry, skipping it if already present
for (var i=1, p = entryCount; i < p; ++i) {
$groupedLights = $('.entry-' + i);
if(!$('.entry-' + i +':eq(0) span').length){
$groupedLights.eq(0)
.append('<span>' + puzz.data[i].position + '</span>');
}
}
util.highlightEntry();
util.highlightClue();
$('.active').eq(0).focus();
$('.active').eq(0).select();
},
/*
- Checks current entry input group value against answer
- If not complete, auto-selects next input for user
*/
checkAnswer: function(e) {
var valToCheck, currVal;
util.getActivePositionFromClassGroup($(e.target));
valToCheck = puzz.data[activePosition].answer.toLowerCase();
currVal = $('.position-' + activePosition + ' input')
.map(function() {
return $(this)
.val()
.toLowerCase();
})
.get()
.join('');
//console.log(currVal + " " + valToCheck);
if(valToCheck === currVal){
$('.active')
.addClass('done')
.removeClass('active');
$('.clues-active').addClass('clue-done');
solved.push(valToCheck);
solvedToggle = true;
return;
}
currOri === 'across' ? nav.nextPrevNav(e, 39) : nav.nextPrevNav(e, 40);
//z++;
//console.log(z);
//console.log('checkAnswer() solvedToggle: '+solvedToggle);
}
}; // end puzInit object
var nav = {
nextPrevNav: function(e, override) {
var len = $actives.length,
struck = override ? override : e.which,
el = $(e.target),
p = el.parent(),
ps = el.parents(),
selector;
util.getActivePositionFromClassGroup(el);
util.highlightEntry();
util.highlightClue();
$('.current').removeClass('current');
selector = '.position-' + activePosition + ' input';
//console.log('nextPrevNav activePosition & struck: '+ activePosition + ' '+struck);
// move input focus/select to 'next' input
switch(struck) {
case 39:
p
.next()
.find('input')
.addClass('current')
.select();
break;
case 37:
p
.prev()
.find('input')
.addClass('current')
.select();
break;
case 40:
ps
.next('tr')
.find(selector)
.addClass('current')
.select();
break;
case 38:
ps
.prev('tr')
.find(selector)
.addClass('current')
.select();
break;
default:
break;
}
},
updateByNav: function(e) {
var target;
$('.clues-active').removeClass('clues-active');
$('.active').removeClass('active');
$('.current').removeClass('current');
currIndex = 0;
target = e.target;
activePosition = $(e.target).data('position');
util.highlightEntry();
util.highlightClue();
$('.active').eq(0).focus();
$('.active').eq(0).select();
$('.active').eq(0).addClass('current');
// store orientation for 'smart' auto-selecting next input
currOri = $('.clues-active').parent('ol').prop('id');
activeClueIndex = $(clueLiEls).index(e.target);
//console.log('updateByNav() activeClueIndex: '+activeClueIndex);
},
// Sets activePosition var and adds active class to current entry
updateByEntry: function(e, next) {
var classes, next, clue, e1Ori, e2Ori, e1Cell, e2Cell;
if(e.keyCode === 9 || next){
// handle tabbing through problems, which keys off clues and requires different handling
activeClueIndex = activeClueIndex === clueLiEls.length-1 ? 0 : ++activeClueIndex;
$('.clues-active').removeClass('.clues-active');
next = $(clueLiEls[activeClueIndex]);
currOri = next.parent().prop('id');
activePosition = $(next).data('position');
// skips over already-solved problems
util.getSkips(activeClueIndex);
activePosition = $(clueLiEls[activeClueIndex]).data('position');
} else {
activeClueIndex = activeClueIndex === clueLiEls.length-1 ? 0 : ++activeClueIndex;
util.getActivePositionFromClassGroup(e.target);
clue = $('#puzzle-clues li' + '[data-position=' + activePosition + ']');
activeClueIndex = $(clueLiEls).index(clue);
currOri = clue.parent().prop('id');
}
util.highlightEntry();
util.highlightClue();
//$actives.eq(0).addClass('current');
//console.log('nav.updateByEntry() reports activePosition as: '+activePosition);
}
}; // end nav object
var util = {
highlightEntry: function() {
// this routine needs to be smarter because it doesn't need to fire every time, only
// when activePosition changes
$actives = $('.active');
$actives.removeClass('active');
$actives = $('.position-' + activePosition + ' input').addClass('active');
$actives.eq(0).focus();
$actives.eq(0).select();
},
highlightClue: function() {
var clue;
$('.clues-active').removeClass('clues-active');
$('#puzzle-clues li' + '[data-position=' + activePosition + ']').addClass('clues-active');
if (mode === 'interacting') {
clue = $('#puzzle-clues li' + '[data-position=' + activePosition + ']');
activeClueIndex = $(clueLiEls).index(clue);
};
},
getClasses: function(light, type) {
if (!light.length) return false;
var classes = $(light).prop('class').split(' '),
classLen = classes.length,
positions = [];
// pluck out just the position classes
for(var i=0; i < classLen; ++i){
if (!classes[i].indexOf(type) ) {
positions.push(classes[i]);
}
}
return positions;
},
getActivePositionFromClassGroup: function(el){
classes = util.getClasses($(el).parent(), 'position');
if(classes.length > 1){
// get orientation for each reported position
e1Ori = $('#puzzle-clues li' + '[data-position=' + classes[0].split('-')[1] + ']').parent().prop('id');
e2Ori = $('#puzzle-clues li' + '[data-position=' + classes[1].split('-')[1] + ']').parent().prop('id');
// test if clicked input is first in series. If so, and it intersects with
// entry of opposite orientation, switch to select this one instead
e1Cell = $('.position-' + classes[0].split('-')[1] + ' input').index(el);
e2Cell = $('.position-' + classes[1].split('-')[1] + ' input').index(el);
if(mode === "setting ui"){
currOri = e1Cell === 0 ? e1Ori : e2Ori; // change orientation if cell clicked was first in a entry of opposite direction
}
if(e1Ori === currOri){
activePosition = classes[0].split('-')[1];
} else if(e2Ori === currOri){
activePosition = classes[1].split('-')[1];
}
} else {
activePosition = classes[0].split('-')[1];
}
console.log('getActivePositionFromClassGroup activePosition: '+activePosition);
},
checkSolved: function(valToCheck) {
for (var i=0, s=solved.length; i < s; i++) {
if(valToCheck === solved[i]){
return true;
}
}
},
getSkips: function(position) {
if ($(clueLiEls[position]).hasClass('clue-done')){
activeClueIndex = position === clueLiEls.length-1 ? 0 : ++activeClueIndex;
util.getSkips(activeClueIndex);
} else {
return false;
}
}
}; // end util object
puzInit.init();
}
})(jQuery);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment