crossword-8.x-1.x-dev/js/classes.js
js/classes.js
(function ($, Drupal, once, drupalSettings) {
Drupal.Crossword = {
Square: function(data, answer, Crossword) {
this.Crossword = Crossword;
this.row = data.row;
this.column = data.col;
// Note that a fill of null means a black square
// while an empty string indicates a redacted answer.
this.fill = data.fill;
// For the answer, black squares contain empty string.
this.answer = answer ? answer : "";
this.numeral = data.numeral;
this.across = data.across ? data.across.index : null;
this.down = data.down ? data.down.index : null;
this.moves = {
'up' : false,
'down' : false,
'left' : false,
'right' : false,
};
this.$square = null;
this.connect = function($square) {
this.$square = $square;
Crossword.sendAnswerEvents(this);
}
// A black or redacted square cannot have an error.
this.hasError = function() {
return this.fill && this.answer && this.answer.toUpperCase() !== this.fill.toUpperCase();
}
// A black square is considered correct.
this.isCorrect = function() {
return this.isBlack() || (this.answer && this.fill && this.answer.toUpperCase() === this.fill.toUpperCase());
}
this.isEmpty = function() {
return !this.isBlack() && this.answer === "";
}
this.isBlack = function() {
return this.fill === null;
}
this.isRedacted = function() {
return this.fill === "";
}
this.isLastLetter = function(dir) {
return this == this[dir]['squares'][this[dir]['squares'].length - 1];
}
this.isFirstLetter = function(dir) {
return this == this[dir]['squares'][0];
}
},
Clue: function(data) {
this.text = data.text;
this.dir = data.dir;
this.index = data.index;
this.numeral = data.numeral;
this.references = data.references; //starts as contstants. objects get added later
this.squares = [];
this.$clue = null;
this.connect = function($clue) {
this.$clue = $clue;
}
this.hasError = function() {
for (var i = 0; i < this.squares.length; i++) {
if (this.squares[i].hasError()) {
return true;
}
}
return false;
}
this.isCorrect = function() {
for (var i = 0; i < this.squares.length; i++) {
if (!this.squares[i].isCorrect()) {
return false;
}
}
return true;
}
this.isComplete = function() {
for (var i = 0; i < this.squares.length; i++) {
if (this.squares[i].isEmpty()) {
return false;
}
}
return true;
}
this.getAriaCurrentString = function() {
var aria = "";
var countString = this.squares.length + " letters.";
var blank = true;
for (var i = 0; i < this.squares.length; i++) {
if (this.squares[i].answer) {
aria += this.squares[i].answer.toUpperCase();
blank = false;
}
else {
aria += "blank";
}
aria += ". ";
}
if (blank) {
return countString;
}
else {
aria = aria.substring(0, aria.length - 1);
return "Answer: " + countString + " " + aria;
}
}
this.getAriaClueText = function() {
return this.numeral + " " + this.dir + ". " + this.text.replace(/_{2,}/, "blank");
}
this.getAnswerLength = function() {
return this.squares.length;
}
},
Crossword: function(data, answers) {
var Crossword = this;
this.data = data;
this.id = data.id;
this.dir = 'across';
this.activeSquare = {'row' : null, 'col': null};
this.activeClue = null;
this.activeReferences = [];
this.answers = answers ? answers : emptyAnswers(); //the initial answers
this.grid = makeGrid(this.answers);
this.clues = makeClues();
connectCluesAndSquares();
this.stack = {
'undo' : [],
'redo' : [],
};
this.$crossword = null;
this.$activeCluesText = null;
this.solved = false;
this.revealed = data.revealed;
this.setActiveSquare = function(Square) {
if (Square.fill !== null) {
this.sendOffEvents();
this.activeSquare = Square;
if (!Square[this.dir]) {
// Uncrossed square. Switch to useful direction.
this.dir = this.dir == 'across' ? 'down' : 'across';
}
this.activeClue = Square[this.dir];
this.activeReferences = Square[this.dir] ? Square[this.dir].references : [];
this.sendOnEvents();
}
return this;
}
this.setActiveClue = function(Clue) {
this.sendOffEvents();
this.activeClue = Clue;
this.dir = Clue.dir;
this.activeSquare = Clue.squares[0];
this.activeReferences = Clue.references;
this.sendOnEvents();
return this;
}
this.changeDir = function() {
this.dir = this.dir == 'across' ? 'down' : 'across';
this.setActiveSquare(this.activeSquare);
return this;
}
// "Move" is called directly by arrow keys.
this.moveActiveSquare = function(move) {
if (this.activeSquare.moves[move]) {
this.setActiveSquare(this.activeSquare.moves[move]);
}
return this;
}
// Advance and retreat are used with movement that is NOT triggered
// by the arrow keys.
// If on last letter, go to next clue.
this.advanceActiveSquare = function() {
if (this.activeSquare.isLastLetter(this.dir)) {
this.advanceToNextUnsolvedClue();
}
else {
if (this.dir == 'across') {
this.moveActiveSquare('right');
}
else {
this.moveActiveSquare('down');
}
}
return this;
}
// Retreat stops at the first letter.
this.retreatActiveSquare = function() {
if (this.activeSquare.isFirstLetter(this.dir)) {
return this;
}
else {
if (this.dir == 'across') {
this.moveActiveSquare('left');
}
else {
this.moveActiveSquare('up');
}
}
return this;
}
this.advanceActiveClue = function() {
if (this.activeClue) {
if (this.clues[this.dir][this.activeClue.index + 1]) {
this.setActiveClue(this.clues[this.dir][this.activeClue.index + 1]);
}
else {
this.setActiveClue(this.clues[this.dir][0]);
}
}
else {
this.setActiveClue(this.clues[this.dir][0]);
}
return this;
}
this.retreatActiveClue = function() {
if (this.activeClue) {
if (this.clues[this.dir][this.activeClue.index - 1]) {
this.setActiveClue(this.clues[this.dir][this.activeClue.index - 1]);
}
else {
this.setActiveClue(this.clues[this.dir][this.clues[this.dir].length - 1]);
}
}
else {
this.setActiveClue(this.clues[this.dir][0]);
}
return this;
}
this.changeActiveClue = function(dir, change) {
// change will be +/- 1
if (dir == this.dir) {
change > 0 ? this.advanceActiveClue() : this.retreatActiveClue();
}
else {
this.changeDir();
}
return this;
}
this.advanceToNextUnsolvedClue = function(goToNextClue = false) {
if (this.activeClue) {
// Check the rest of clues in this direction.
for (var i = this.activeClue.index + 1; i < this.clues[this.dir].length; i++) {
if (this.clues[this.dir][i]) {
if (goToNextClue || (this.showingErrors() && this.clues[this.dir][i].hasError()) || !this.clues[this.dir][i].isComplete()) {
this.setActiveClue(this.clues[this.dir][i]);
return this;
}
}
}
// Go to other direction and check all clues.
var other_dir = this.dir == 'across' ? 'down' : 'across';
for (var i = 0; i < this.clues[other_dir].length; i++) {
if (this.clues[other_dir][i]) {
if (goToNextClue || (this.showingErrors() && this.clues[other_dir][i].hasError()) || !this.clues[other_dir][i].isComplete()) {
this.setActiveClue(this.clues[other_dir][i]);
return this;
}
}
}
// Check start of clues in this direction.
for (var i = 0; i < this.activeClue.index; i++) {
if (this.clues[this.dir][i]) {
if ((this.showingErrors() && this.clues[this.dir][i].hasError()) || !this.clues[this.dir][i].isComplete()) {
this.setActiveClue(this.clues[this.dir][i]);
return this;
}
}
}
}
else {
this.setActiveClue(this.clues[this.dir][0]);
}
// If we haven't returned we simply advance to the next clue.
return this.advanceToNextUnsolvedClue(true);
}
this.retreatToPreviousUnsolvedClue = function(goToPreviousClue = false) {
if (this.activeClue) {
// Check the rest of clues in this direction.
for (var i = this.activeClue.index - 1; i >= 0; i--) {
if (this.clues[this.dir][i]) {
if (goToPreviousClue || (this.showingErrors() && this.clues[this.dir][i].hasError()) || !this.clues[this.dir][i].isComplete()) {
this.setActiveClue(this.clues[this.dir][i]);
return this;
}
}
}
// Go to other direction and check all clues.
var other_dir = this.dir == 'across' ? 'down' : 'across';
for (var i = this.clues[other_dir].length - 1; i >= 0; i--) {
if (this.clues[other_dir][i]) {
if (goToPreviousClue || (this.showingErrors() && this.clues[other_dir][i].hasError()) || !this.clues[other_dir][i].isComplete()) {
this.setActiveClue(this.clues[other_dir][i]);
return this;
}
}
}
// Check end of clues in this direction.
for (var i = this.clues[this.dir].length - 1; i > this.activeClue.index; i--) {
if (this.clues[this.dir][i]) {
if (goToPreviousClue || (this.showingErrors() && this.clues[this.dir][i].hasError()) || !this.clues[this.dir][i].isComplete()) {
this.setActiveClue(this.clues[this.dir][i]);
return this;
}
}
}
}
else {
this.setActiveClue(this.clues[this.dir][0]);
}
// If we haven't returned we simply advance to the previous clue.
return this.retreatToPreviousUnsolvedClue(true);
}
this.focus = function() {
if (this.activeSquare && this.activeSquare['$square']) {
this.activeSquare['$square'].trigger('crossword-focus');
}
return this;
}
this.escape = function() {
this.sendOffEvents();
this.activeClue = null;
this.activeSquare = null
this.activeReferences = null;
return this;
}
this.setAnswer = function(letter, rebus, undo, redo) {
if (rebus) {
// Rebus means there can be more than one letter in the square.
// Backspace gets handled special.
if (!letter) {
// Backspace.
this.activeSquare.answer = this.activeSquare.answer ? this.activeSquare.answer.substring(0, this.activeSquare.answer.length - 1) : "";
}
else {
// Not backspace.
this.activeSquare.answer = this.activeSquare.answer ? this.activeSquare.answer + letter : letter;
}
if (!(undo || redo)) {
this.stack.undo.push({'square' : this.activeSquare, 'letter' : this.activeSquare.answer});
this.stack.redo = [];
}
this.sendAnswerEvents(this.activeSquare);
}
else {
if (!(undo || redo)) {
this.stack.undo.push({'square' : this.activeSquare, 'letter' : this.activeSquare.answer});
this.stack.redo = [];
}
this.activeSquare.answer = letter;
this.sendAnswerEvents(this.activeSquare);
if (!undo) {
if (letter === "") {
this.retreatActiveSquare();
}
else {
this.advanceActiveSquare();
}
}
}
return this;
}
this.cheat = function() {
if (this.activeSquare && this.activeSquare.fill) {
this.sendCheatEvents(this.activeSquare);
this.setAnswer(this.activeSquare.fill);
}
return this;
}
this.undo = function() {
if (this.stack.undo.length) {
var oldState = this.stack.undo.pop();
this.stack.redo.push({'square' : oldState.square, 'letter' : oldState.square.answer });
this.setActiveSquare(oldState.square);
this.setAnswer(oldState.letter, false, true);
}
return this;
}
this.redo = function() {
if (this.stack.redo.length) {
var oldState = this.stack.redo.pop();
this.stack.undo.push({'square' : oldState.square, 'letter' : oldState.square.answer });
this.setActiveSquare(oldState.square);
this.setAnswer(oldState.letter, false, false, true);
}
return this;
}
this.reveal = function() {
this.revealed = true;
for (var row_index = 0; row_index < this.grid.length; row_index++) {
for (var col_index = 0; col_index < this.grid[row_index].length; col_index++) {
var Square = this.grid[row_index][col_index];
if (Square.isRedacted()) {
// We jump out if puzzle is redacted.
return this;
}
if (!Square.isBlack() && !Square.isRedacted() && Square.answer.toUpperCase() !== Square.fill.toUpperCase()) {
Square.answer = Square.fill;
this.sendCheatEvents(Square);
this.sendAnswerEvents(Square);
}
}
}
if (!this.solved && this.$crossword) {
this.$crossword.trigger('crossword-revealed');
}
return this;
}
// Note that this function should return an array that matches the format
// of the array returned by CrosswordDataService::getSolution(). The idea
// is to use getAnswers() on the FE and getSolution() on the BE in the
// scenario where the solution is not exposed publicly to the FE.
this.getAnswers = function() {
var answers = [];
for (var $row_index = 0; $row_index < this.grid.length; $row_index++) {
answers[$row_index] = [];
for (var $col_index = 0; $col_index < this.grid[$row_index].length; $col_index++) {
answers[$row_index][$col_index] = this.grid[$row_index][$col_index].answer;
}
}
return answers;
}
this.clear = function() {
this.setAnswers(emptyAnswers());
this.revealed = false;
this.solved = false;
if (this.$crossword) {
this.$crossword.trigger('crossword-clear');
}
return this;
}
this.setAnswers = function(answers) {
this.stack.undo = [];
for (var $row_index = 0; $row_index < this.grid.length; $row_index++) {
for (var $col_index = 0; $col_index < this.grid[$row_index].length; $col_index++) {
this.grid[$row_index][$col_index].answer = answers[$row_index][$col_index];
this.sendAnswerEvents(this.grid[$row_index][$col_index]);
}
}
return this;
}
this.isSolved = function() {
for (var $row_index = 0; $row_index < this.grid.length; $row_index++) {
for (var $col_index = 0; $col_index < this.grid[$row_index].length; $col_index++) {
var Square = this.grid[$row_index][$col_index];
// If any squares are redacted, this can't decide that it's solved.
if (Square.isRedacted() || (!Square.isCorrect() && !Square.isBlack())) {
return false;
}
}
}
this.solved = true;
return true;
}
this.showingErrors = function() {
return this.$crossword.hasClass('show-errors');
}
this.countBlankSquares = function() {
var count = 0;
for (var $row_index = 0; $row_index < this.grid.length; $row_index++) {
for (var $col_index = 0; $col_index < this.grid[$row_index].length; $col_index++) {
if (this.grid[$row_index][$col_index].isEmpty()) {
count++;
}
}
}
return count;
}
/**
* Functions that trigger events on dom elements.
*/
this.sendOffEvents = function(){
if (this.activeClue) {
if (this.$activeCluesText) {
this.$activeCluesText.trigger('crossword-off');
}
if (this.activeClue['$clue']) {
this.activeClue['$clue'].trigger('crossword-off');
}
this.activeClue.squares.forEach(function(item, index){
if (item['$square']) {
item['$square'].trigger('crossword-off');
}
});
if(this.activeReferences) {
this.activeReferences.forEach(function(clue, index){
if (clue['$clue']) {
clue['$clue'].trigger('crossword-off');
}
clue.squares.forEach(function(item, index){
if (item['$square']) {
item['$square'].trigger('crossword-off');
}
});
});
}
}
if (this.activeSquare && this.activeSquare['$square']) {
this.activeSquare['$square'].trigger('crossword-off');
}
}
this.sendOnEvents = function(){
if (this.activeClue) {
if (this.$activeCluesText) {
this.$activeCluesText.trigger('crossword-active', [this.activeClue]);
}
if (this.activeClue['$clue']) {
this.activeClue['$clue'].trigger('crossword-active');
}
this.activeClue.squares.forEach(function(item, index){
if (item['$square']) {
item['$square'].trigger('crossword-highlight');
}
});
if(this.activeReferences) {
this.activeReferences.forEach(function(clue, index){
if (clue['$clue']) {
clue['$clue'].trigger('crossword-reference');
}
clue.squares.forEach(function(item, index){
if (item['$square']) {
item['$square'].trigger('crossword-reference');
}
});
});
}
}
if (this.activeSquare && this.activeSquare['$square']) {
this.activeSquare['$square'].trigger('crossword-active');
}
}
this.sendAnswerEvents = function(Square){
if (Square && Square['$square']) {
Square['$square'].trigger('crossword-answer', [Square.answer]);
if (Square.hasError()) {
Square['$square'].trigger('crossword-error');
}
else {
Square['$square'].trigger('crossword-ok');
}
if (Square.answer.length > 1) {
Square['$square'].trigger('crossword-rebus');
}
else {
Square['$square'].trigger('crossword-not-rebus');
}
// now the clues
if (Square.down && Square.down['$clue']) {
Square.down['$clue'].trigger('crossword-aria-update');
if (Square.down.hasError()) {
Square.down['$clue'].trigger('crossword-error');
}
else {
Square.down['$clue'].trigger('crossword-ok');
}
if (Square.down.isComplete()) {
Square.down['$clue'].trigger('crossword-clue-complete');
}
else {
Square.down['$clue'].trigger('crossword-clue-not-complete');
}
}
if (Square.across && Square.across['$clue']) {
Square.across['$clue'].trigger('crossword-aria-update');
if (Square.across.hasError()) {
Square.across['$clue'].trigger('crossword-error');
}
else {
Square.across['$clue'].trigger('crossword-ok');
}
if (Square.across.isComplete()) {
Square.across['$clue'].trigger('crossword-clue-complete');
}
else {
Square.across['$clue'].trigger('crossword-clue-not-complete');
}
}
}
if (!this.solved && !this.revealed && !Square.isRedacted() && this.isSolved()) {
if (this.$crossword) {
this.$crossword.trigger('crossword-solved');
}
}
}
this.sendCheatEvents = function(Square){
if (Square && Square['$square']) {
Square['$square'].trigger('crossword-cheat');
if (Square.across && Square.across['$clue']) {
Square.across['$clue'].trigger('crossword-cheat');
}
if (Square.down && Square.down['$clue']) {
Square.down['$clue'].trigger('crossword-cheat');
}
}
}
/**
* Internal functions for initialization.
*/
function emptyAnswers() {
var grid = Crossword.data.puzzle.grid;
var answers = [];
for (var row_index = 0; row_index < grid.length; row_index++) {
answers.push([]);
for (var col_index = 0; col_index < grid[row_index].length; col_index++) {
answers[row_index].push('');
}
}
return answers;
}
function makeGrid(answers) {
var grid = [];
var data_grid = Crossword.data.puzzle.grid;
// start by creating objects
for (var row_index = 0; row_index < data_grid.length; row_index++) {
var row = [];
for (var col_index = 0; col_index < data_grid[row_index].length; col_index++) {
row[col_index] = new Drupal.Crossword.Square(data_grid[row_index][col_index], answers[row_index][col_index], Crossword);
}
grid.push(row);
}
// now connect the moves
for (var row_index = 0; row_index < data_grid.length; row_index++) {
for (var col_index = 0; col_index < data_grid[row_index].length; col_index++) {
var square = grid[row_index][col_index];
for (move in data_grid[row_index][col_index]['moves']) {
if (data_grid[row_index][col_index]['moves'][move]) {
square.moves[move] = grid[data_grid[row_index][col_index]['moves'][move].row][data_grid[row_index][col_index]['moves'][move].col];
}
}
}
}
return grid;
}
function makeClues() {
var clues = {
'across' : [],
'down' : [],
};
var dirs = {'across' : true, 'down' : true};
for (var dir in dirs) {
var data_clues = Crossword.data.puzzle.clues[dir];
for (var i = 0; i < data_clues.length; i++) {
data_clues[i].index = i;
data_clues[i].dir = dir;
clues[dir].push(new Drupal.Crossword.Clue(data_clues[i]));
}
}
// connect references
for (var dir in dirs) {
for (var i = 0; i < clues[dir].length; i++) {
if (clues[dir][i].references) {
var realRefs = [];
var refs = clues[dir][i].references
for (var ref_index in refs) {
realRefs.push(clues[refs[ref_index].dir][refs[ref_index].index]);
}
clues[dir][i].references = realRefs;
}
}
}
return clues;
}
function connectCluesAndSquares() {
var grid = Crossword.grid;
var clues = Crossword.clues;
var dirs = {'across' : true, 'down' : true};
for (var row_index = 0; row_index < grid.length; row_index++) {
for (var col_index = 0; col_index < grid[row_index].length; col_index++) {
var Square = grid[row_index][col_index];
for (var dir in dirs) {
if (Square[dir] !== null) {
clues[dir][Square[dir]]['squares'].push(Square);
Square[dir] = clues[dir][Square[dir]];
}
}
}
}
}
// A funny thing for initialization that doesn't have anywhere nice to go.
this.setActiveClue(this.clues.across[0]);
}
}
})(jQuery, Drupal, once, drupalSettings);
