diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..6a81a4c
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,36 @@
+version: 2
+jobs:
+ test:
+ docker:
+ - image: circleci/node:10-browsers
+ steps:
+ - checkout
+ - run: npm config set prefix "$HOME/.local"
+ - run: npm i -g origami-build-tools@^8
+ - run: $HOME/.local/bin/obt install
+ - run: $HOME/.local/bin/obt demo --demo-filter pa11y --suppress-errors
+ - run: $HOME/.local/bin/obt verify
+ - run: $HOME/.local/bin/obt test
+ - run: git clean -fxd
+ - run: npx occ 0.0.0
+ - run: $HOME/.local/bin/obt install --ignore-bower
+ - run: $HOME/.local/bin/obt test --ignore-bower
+ publish_to_npm:
+ docker:
+ - image: circleci/node:10
+ steps:
+ - checkout
+ - run: npx occ ${CIRCLE_TAG##v}
+ - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > $HOME/.npmrc
+ - run: npm publish --access public
+workflows:
+ version: 2
+ test:
+ jobs:
+ - test
+ - publish_to_npm:
+ filters:
+ tags:
+ only: /^v.*/
+ branches:
+ ignore: /.*/
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 0a28456..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "env": {
- "browser": true,
- "es6": true,
- "mocha": true,
- "node": true
- },
- "ecmaFeatures": {
- "modules": false
- },
- "rules": {
- "no-unused-vars": 2,
- "no-undef": 2,
- "eqeqeq": 2,
- "no-underscore-dangle": 0,
- "guard-for-in": 2,
- "no-extend-native": 2,
- "wrap-iife": 2,
- "new-cap": 2,
- "no-caller": 2,
- "quotes": [1, "single"],
- "no-loop-func": 2,
- "no-irregular-whitespace": 1,
- "no-multi-spaces": 2,
- "one-var": [2, "never"],
- "no-var": 1,
- "strict": [1, "global"],
- "no-console": 1,
- "semi": 1,
- "indent": [2, "tab"],
- "no-trailing-spaces": 2
- }
-}
diff --git a/bower.json b/bower.json
index 4e37f0b..deaa440 100644
--- a/bower.json
+++ b/bower.json
@@ -1,12 +1,12 @@
{
"name": "o-crossword",
"dependencies": {
- "o-colors": "^4.1.5",
- "o-grid": "^4.0.6",
- "o-typography": "^5.4.1",
- "o-viewport": "^3.1.6",
- "o-tracking": "^1.3.5",
- "o-buttons": "^5.8.5"
+ "o-colors": "^4.8.5",
+ "o-grid": "^4.5.1",
+ "o-typography": "^5.10.1",
+ "o-viewport": "^3.3.2",
+ "o-tracking": "^1.6.0",
+ "o-buttons": "^5.16.2"
},
"main": [
"main.scss",
@@ -20,6 +20,6 @@
"package.json"
],
"resolutions": {
- "o-colors": "^4.1.5"
+ "o-colors": "^4.8.5"
}
}
diff --git a/circle.yml b/circle.yml
deleted file mode 100644
index f83851b..0000000
--- a/circle.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-machine:
- node:
- version: 4.2.2
- post:
- - npm install -g Financial-Times/origami-build-tools#node4
-dependencies:
- override:
- - obt install
- cache_directories:
- - "node_modules"
-test:
- override:
- - obt test
- - obt verify
diff --git a/demos/src/demo.js b/demos/src/demo.js
index 47891e6..1ef565b 100644
--- a/demos/src/demo.js
+++ b/demos/src/demo.js
@@ -1,5 +1,5 @@
require('../../main.js');
document.addEventListener("DOMContentLoaded", function() {
- document.dispatchEvent(new CustomEvent('o.DOMContentLoaded'));
+ document.dispatchEvent(new CustomEvent('o.DOMContentLoaded'));
});
diff --git a/karma.conf.js b/karma.conf.js
deleted file mode 100644
index 5646346..0000000
--- a/karma.conf.js
+++ /dev/null
@@ -1,97 +0,0 @@
-/*global module*/
-
-module.exports = function(config) {
- config.set({
-
- // base path that will be used to resolve all patterns (eg. files, exclude)
- basePath: '',
-
-
- // frameworks to use
- // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
- frameworks: ['mocha'],
-
-
- plugins: [
- 'karma-mocha',
- 'karma-phantomjs-launcher',
- 'karma-webpack'
- ],
-
-
- // list of files / patterns to load in the browser
- files: [
- 'https://cdn.polyfill.io/v2/polyfill.js?flags=gated',
- 'test/*.test.js'
- ],
-
-
- // list of files to exclude
- exclude: [
- ],
-
-
- // preprocess matching files before serving them to the browser
- // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
- preprocessors: {
- 'test/*.test.js': ['webpack']
- },
-
-
- // test results reporter to use
- // possible values: 'dots', 'progress'
- // available reporters: https://npmjs.org/browse/keyword/karma-reporter
- reporters: ['progress'],
-
-
- // web server port
- port: 9876,
-
-
- // enable / disable colors in the output (reporters and logs)
- colors: true,
-
-
- // level of logging
- // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
- logLevel: config.LOG_INFO,
-
-
- // enable / disable watching file and executing tests whenever any file changes
- autoWatch: true,
-
-
- // start these browsers
- // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
- browsers: ['PhantomJS'],
-
-
- // Continuous Integration mode
- // if true, Karma captures browsers, runs the tests and exits
- singleRun: true,
-
- webpack: {
- quiet: true,
- module: {
- loaders: [
- {
- test: /\.js$/,
- exclude: /node_modules/,
- loaders: [
- 'babel?optional[]=runtime',
- 'imports?define=>false'
- ]
- }
- ],
- noParse: [
- /\/sinon\.js/,
- ]
- }
- },
-
- // Hide webpack output logging
- webpackMiddleware: {
- noInfo: true
- }
- });
-};
diff --git a/main.js b/main.js
index 779b36e..a5ce339 100644
--- a/main.js
+++ b/main.js
@@ -2,7 +2,9 @@
const OCrossword = module.exports = require('./src/js/oCrossword');
const constructAll = function() {
- if (OCrossword.disableAutoInit) return;
+ if (OCrossword.disableAutoInit) {
+ return;
+ }
[].slice.call(document.querySelectorAll('[data-o-component~="o-crossword"]')).forEach(function (el) {
new OCrossword(el);
});
diff --git a/main.scss b/main.scss
index 807ddc5..4313341 100644
--- a/main.scss
+++ b/main.scss
@@ -1,6 +1,4 @@
$o-crossword-is-silent: true !default;
-$o-buttons-is-silent: false;
-
@import "o-colors/main";
@import "o-grid/main";
@import "o-typography/main";
@@ -12,6 +10,7 @@ $o-buttons-is-silent: false;
@import "src/scss/base";
@import "src/scss/orientation";
@import "src/scss/print";
+@import "src/scss/mobile";
@if ($o-crossword-is-silent == false) {
@include oCrosswordAll;
@@ -20,4 +19,3 @@ $o-buttons-is-silent: false;
$o-crossword-is-silent: true !global;
}
-@import "src/scss/mobile";
diff --git a/origami.json b/origami.json
index ddb4899..8cf7488 100644
--- a/origami.json
+++ b/origami.json
@@ -19,7 +19,8 @@
"requestAnimationFrame",
"Node.prototype.contains",
"Array.prototype.find",
- "Array.prototype.filter"
+ "Array.prototype.filter",
+ "Promise"
]
},
"demosDefaults": {
@@ -29,14 +30,15 @@
"demos": [
{
"name": "basic",
+ "title": "basic",
+ "hidden": true,
"template": "demos/src/basic.mustache",
- "expanded": true,
"description": ""
},
{
"name": "answered",
+ "title": "answered",
"template": "demos/src/answered.mustache",
- "expanded": false,
"description": ""
}
]
diff --git a/package.json b/package.json
deleted file mode 100644
index ced3527..0000000
--- a/package.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "name": "o-crossword",
- "version": "0.3.0",
- "description": "FT-branded crosswords",
- "devDependencies": {
- "babel-loader": "^5.3.2",
- "babel-runtime": "5.8.25",
- "chai": "^3.5.0",
- "chai-dom": "^1.4.0",
- "imports-loader": "^0.6.4",
- "karma": "^0.13.0",
- "karma-mocha": "^0.2.2",
- "karma-phantomjs-launcher": "^1.0.0",
- "karma-webpack": "^1.7.0",
- "mocha": "^2.4.5",
- "phantomjs": "^1.9.16",
- "sinon": "^1.17.3",
- "webpack": "^1.12.2"
- },
- "private": true,
- "scripts": {
- "test": "./node_modules/karma/bin/karma start karma.conf.js"
- }
-}
diff --git a/src/js/crossword_parser.js b/src/js/crossword_parser.js
index 4b18c89..a12b7f7 100644
--- a/src/js/crossword_parser.js
+++ b/src/js/crossword_parser.js
@@ -1,509 +1,504 @@
+/* eslint-disable no-cond-assign */
// Using UMD (Universal Module Definition), see https://github.com/umdjs/umd, and Jake,
// for a js file to be included as-is in Node code and in browser code.
(function (root, factory) {
- if (typeof module === 'object' && module.exports) {
- // Node. Does not work with strict CommonJS, but
- // only CommonJS-like environments that support module.exports,
- // like Node.
- module.exports = factory();
- } else {
- // Browser globals (root is window)
- root.CrosswordDSL = factory();
- }
+ if (typeof module === 'object' && module.exports) {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory();
+ } else {
+ // Browser globals (root is window)
+ root.CrosswordDSL = factory();
+ }
}(this, function () {
- // given the DSL, ensure we have all the relevant pieces,
- // and assume there will be subsequent checking to ensure they are valid
- function parseDSL(text){
- var crossword = {
- version : "standard v1",
- author : "",
- editor : "Colin Inman",
- publisher : "Financial Times",
- copyright : "2017, Financial Times",
- pubdate : "today",
- dimensions : "17x17",
- across : [],
- down : [],
- errors : [],
- originalDSL : text,
- };
- var cluesGrouping;
- var lines = text.split(/\r|\n/);
-
- for(let line of lines){
- let match;
- // strip out comments
- if (match = /^([^\#]*)\#.*$/.exec(line) ) {
- line = match[1];
- }
- // strip out trailing and leading spaces
- line = line.trim();
-
- if ( line === "" ) { /* ignore blank lines */ }
- else if( line === "---") { /* ignore front matter lines */ }
- else if (match = /^(layout|tag|tags|permalink):\s/ .exec(line) ) { /* ignore front matter fields */ }
- else if (match = /^version:?\s+(.+)$/i .exec(line) ) { crossword.version = match[1]; }
- else if (match = /^name:?\s+(.+)$/i .exec(line) ) { crossword.name = match[1]; }
- else if (match = /^author:?\s+(.+)$/i .exec(line) ) { crossword.author = match[1]; }
- else if (match = /^editor:?\s+(.+)$/i .exec(line) ) { crossword.editor = match[1]; }
- else if (match = /^copyright:?\s+(.+)$/i .exec(line) ) { crossword.copyright = match[1]; }
- else if (match = /^publisher:?\s+(.+)$/i .exec(line) ) { crossword.publisher = match[1]; }
- else if (match = /^pubdate:?\s+(\d{4}\/\d\d\/\d\d)$/i.exec(line) ) { crossword.pubdate = match[1]; }
- else if (match = /^(?:size|dimensions):?\s+(15x15|17x17)$/i.exec(line) ) { crossword.dimensions = match[1]; }
- else if (match = /^(across|down):?$/i .exec(line) ) { cluesGrouping = match[1]; }
- else if (match = /^-\s\((\d+),(\d+)\)\s+(\d+)\.\s+(.+)\s+\(([A-Z,\-*]+|[0-9,-]+)\)$/.exec(line) ) {
- if (! /(across|down)/.test(cluesGrouping)) {
- crossword.errors.push("ERROR: clue specified but no 'across' or 'down' grouping specified");
- break;
- } else {
- let clue = {
- coordinates : [ parseInt(match[1]), parseInt(match[2]) ],
- id : parseInt(match[3]),
- body : match[4],
- answerCSV : match[5], // could be in the form of either "A,LIST-OF,WORDS" or "1,4-2,5"
- original : line,
- };
- crossword[cluesGrouping].push(clue);
- }
- } else {
- crossword.errors.push("ERROR: couldn't parse line: " + line);
- }
- };
-
- return crossword;
- }
-
- // having found the pieces, check that they encode a valid crossword,
- // creating useful data structures along the way
- function validateAndEmbellishCrossword( crossword ){
- var maxCoord = parseInt(crossword.dimensions.split('x')[0]);
- crossword.maxCoord = maxCoord;
- var grid = new Array( maxCoord * maxCoord ).fill(' ');
- crossword.grid = grid;
- var groupingPrev = {
- across : {
- id : 0,
- x : 0,
- y : 0
- },
- down : {
- id : 0,
- x : 0,
- y : 0
- }
- };
- var knownIds = {};
- crossword.knownIds = knownIds;
- var maxId = 0;
-
- crossword.answers = {
- across : [],
- down : []
- };
-
- // insist on having at least one clue !
- if ( (crossword['across'].length + crossword['down'].length) == 0) {
- crossword.errors.push("Error: no valid clues specified");
- }
-
- for(let grouping of ['across', 'down']){
- let prev = groupingPrev[grouping];
-
- for(let clue of crossword[grouping]){
- function clueError(msg){
- crossword.errors.push("Error: " + msg + " in " + grouping + " clue=" + clue.original);
- }
-
- // check non-zero id
- if (clue.id === 0) {
- clueError("id must be positive");
- break;
- }
-
- maxId = (clue.id > maxId) ? clue.id : maxId;
-
- // check id sequence in order
- if (clue.id <= prev.id) {
- clueError("id out of sequence");
- break;
- }
-
- // check x,y within bounds
- let x = clue.coordinates[0];
- if (x > maxCoord) {
- clueError("x coord too large");
- break;
- }
- let y = clue.coordinates[1];
- if (y > maxCoord) {
- clueError("y coord too large");
- break;
- }
-
- // check all clues with shared ids start at same coords
- if (clue.id in knownIds) {
- let knownCoords = knownIds[clue.id].coordinates;
- if ( x !== knownCoords[0]
- || y !== knownCoords[1]) {
- clueError("shared id clashes with previous coordinates");
- break;
- }
- } else {
- knownIds[clue.id] = clue;
- }
-
- {
- // check answer within bounds
- // and unpack the answerCSV
-
- // convert "ANSWER,PARTS-INTO,NUMBERS" into number csv e.g. "6,5-4,6" (etc)
- if ( /^[A-Z,\-*]+$/.test(clue.answerCSV) ) {
- clue.numericCSV = clue.answerCSV.replace(/[A-Z*]+/g, match => {return match.length.toString() } );
- } else {
- clue.numericCSV = clue.answerCSV;
- }
-
- // and if the answer is solely *s, replace that with the number csv
- if ( /^[*,\-]+$/.test(clue.answerCSV) ) {
- clue.answerCSV = clue.numericCSV;
- }
-
- let answerPieces = clue.answerCSV.split(/[,-]/);
- let words = answerPieces.map(p => {
- if (/^[0-9]+$/.test(p)) {
- let pInt = parseInt(p);
- if (pInt == 0) {
- clueError("answer contains a word size of 0");
- }
- return '*'.repeat( pInt );
- } else {
- if (p.length == 0) {
- clueError("answer contains an empty word");
- }
- return p;
- }
- });
-
- let wordsString = words.join('');
- clue.wordsString = wordsString;
- if (wordsString.length > maxCoord) {
- clueError("answer too long for crossword");
- break;
- }
- crossword.answers[grouping].push(wordsString);
-
- clue.wordsLengths = words.map(function(w){
- return w.length;
- });
- }
-
- // check answer + offset within bounds
- if( (grouping==='across' && (clue.wordsString.length + x - 1 > maxCoord))
- || (grouping==='down' && (clue.wordsString.length + y - 1 > maxCoord)) ){
- clueError("answer too long for crossword from that coord");
- break;
- }
-
- {
- // check answer does not clash with previous answers
- let step = (grouping==='across')? 1 : maxCoord;
- for (var i = 0; i < clue.wordsString.length; i++) {
- let pos = (x-1) + (y-1)*maxCoord + i*step;
- if (grid[pos] === ' ') {
- grid[pos] = clue.wordsString[i];
- } else if( grid[pos] !== clue.wordsString[i] ) {
- clueError("letter " + (i+1) + " clashes with previous clues");
- break;
- }
- }
- }
-
- // update prev
- prev.id = clue.id;
- prev.x = x;
- prev.y = y;
- }
- }
-
- // check we have a contiguous and complete clue id sequence
- if (crossword.errors.length == 0) {
- for (var i = 1; i <= maxId; i++) {
- if (! (i in knownIds)) {
- crossword.errors.push("Error: missing clue with id=" + i);
- }
- }
- }
-
- // check all the clues across and down are monotonic,
- // i.e. each id starts to the right or down from the previous id
- if (crossword.errors.length == 0) {
- for (var i = 2; i <= maxId; i++) {
- let prevClue = knownIds[i-1];
- let clue = knownIds[i];
-
- if ( (clue.coordinates[0] + clue.coordinates[1] * maxCoord) <= (prevClue.coordinates[0] + prevClue.coordinates[1] * maxCoord) ) {
- if (clue.coordinates[1] < prevClue.coordinates[1]) {
- crossword.errors.push("Error: clue " + clue.id + " starts above clue " + prevClue.id);
- } else if ((clue.coordinates[1] === prevClue.coordinates[1]) && (clue.coordinates[0] === prevClue.coordinates[0])) {
- crossword.errors.push("Error: clue " + clue.id + " starts at same coords as clue " + prevClue.id);
- } else {
- crossword.errors.push("Error: clue " + clue.id + " starts to the left of clue " + prevClue.id);
- }
- break;
- }
- }
- }
-
- // check clues start from edge or from an empty cell
-
- return crossword;
- }
-
- function getElementByClass(name) {
- return document.getElementsByClassName(name)[0];
- }
-
- function getElementById(id) {
- return document.getElementById(id);
- }
-
- // a simple text display of the crossword answers in place
- function generateGridText(crossword) {
- var gridText = '';
-
- if('grid' in crossword) {
- let rows = [];
- let maxCoord = crossword.maxCoord;
- let grid = crossword.grid;
-
- {
- let row10s = [' ', ' ', ' '];
- let row1s = [' ', ' ', ' '];
- let rowSpaces = [' ', ' ', ' '];
- for (var x = 1; x <= maxCoord; x++) {
- let num10s = Math.floor(x/10);
- row10s.push((num10s > 0)? num10s : ' ');
- row1s.push(x%10);
- rowSpaces.push(' ');
- }
- rows.push(row10s.join(''));
- rows.push(row1s.join(''));
- rows.push(rowSpaces.join(''));
- }
-
- for (var y = 1; y <= maxCoord; y++) {
- let row = [];
- {
- let num10s = Math.floor(y/10);
- row.push((num10s > 0)? num10s : ' ');
- row.push(y%10);
- row.push(' ');
- }
- for (var x = 1; x <= maxCoord; x++) {
- let cell = grid[(x-1) + (y-1)*maxCoord];
- cell = (cell === " ")? '.' : cell;
- row.push( cell );
- }
- rows.push( row.join('') );
- }
- gridText = rows.join("\n");
- }
-
- return gridText;
- }
-
- // having previously checked that the data encodes a valid crossword,
- // actually construct the spec as a data structure,
- // assuming a later step will convert it to JSON text
- function generateSpec(crossword){
- var spec = {
- name : crossword.name,
- author : crossword.author,
- editor : crossword.editor,
- copyright : crossword.copyright,
- publisher : crossword.publisher,
- date : crossword.pubdate,
- size : {
- rows : crossword.maxCoord,
- cols : crossword.maxCoord,
- },
- grid : [],
- gridnums : [],
- clues : {
- across : [],
- down : [],
- },
- answers : crossword.answers,
- notepad : "",
- id : crossword.name,
- };
-
- // flesh out spec grid
- for (var y = 1; y<=crossword.maxCoord; y++) {
- let row = [];
- for (var x = 1; x<=crossword.maxCoord; x++) {
- let cell = crossword.grid[(x-1) + (y-1)*crossword.maxCoord];
- row.push( (cell === ' ')? '.' : 'X' );
- }
- spec.grid.push(row);
- }
-
- // flesh out gridnums
- // fill with 0, then overwrite with ids
-
- for (var y = 1; y<=crossword.maxCoord; y++) {
- spec.gridnums.push( new Array(crossword.maxCoord).fill(0) );
- }
-
- for (var id in crossword.knownIds) {
- let clue = crossword.knownIds[id];
- spec.gridnums[clue.coordinates[1]-1][clue.coordinates[0]-1] = parseInt(id);
- }
-
- // flesh out clues
-
- ['across', 'down'].forEach( function(grouping){
- crossword[grouping].forEach( function(clue) {
- let item = [
- parseInt(clue.id),
- clue.body + ' (' + clue.numericCSV + ')',
- clue.wordsLengths,
- clue.numericCSV
- ];
- spec.clues[grouping].push(item);
- });
- });
-
- {
- // if the answers are just placeholders (lots of *s or Xs)
- // assume they are not to be displayed,
- // so delete them from the spec
- let concatAllAnswerWordsStrings = spec.answers.across.join('') + spec.answers.down.join('');
- if ( /^(X+|\*+)$/.test(concatAllAnswerWordsStrings) ) {
- delete spec['answers'];
- }
- }
-
- return spec;
- }
-
- // given a crossword obj, generate the DSL for it
- function generateDSL( crossword, withAnswers=true ){
- var lines = [];
- var nonClueFields = [
- 'version', 'name', 'author', 'editor', 'copyright', 'publisher', 'pubdate',
- ];
- nonClueFields.forEach(field => {
- lines.push(`${field}: ${crossword[field]}`);
- });
-
- lines.push(`size: ${crossword.dimensions}`);
-
- ['across', 'down'].forEach( grouping => {
- lines.push(`${grouping}:`);
- crossword[grouping].forEach( clue => {
- var pieces = [
- '-',
- `(${clue.coordinates.join(',')})`,
- `${clue.id}.`,
- clue.body,
- `(${(withAnswers)? clue.answerCSV : clue.numericCSV})`
- ];
- lines.push(pieces.join(' '));
- });
- });
-
- var footerComments = [
- '',
- "Notes on the text format...",
- "Can't use square brackets or speech marks.",
- "A clue has the form",
- "- (COORDINATES) ID. Clue text (ANSWER)",
- "Coordinates of clue in grid are (across,down), so (1,1) = top left, (17,17) = bottom right.",
- "ID is a number, followed by a full stop.",
- "(WORDS,IN,ANSWER): capitalised, and separated by commas or hyphens, or (numbers) separated by commas or hyphens.",
- "ANSWERS with all words of ***** are converted to numbers.",
- ];
- lines = lines.concat( footerComments.map(c => { return `# ${c}`; } ) );
-
- let frontMatterLine = '---';
- lines.unshift( frontMatterLine );
- lines.push ( frontMatterLine );
-
- var dsl = lines.join("\n");
-
- return dsl;
- }
-
- // given some text, decide what format it is (currently, only the DSL)
- // and parse it accordingly,
- // generating the grid text and output format if there are no errors,
- // returning the crossword object with all the bits (or the errors).
- function parseWhateverItIs(text) {
- let crossword = parseDSL(text);
-
- // only attempt to validate the crossword if no errors found so far
- if (crossword.errors.length == 0) {
- crossword = validateAndEmbellishCrossword(crossword);
- console.log("parseWhateverItIs: validated crossword");
- } else {
- console.log("parseWhateverItIs: did not validate crossword=", crossword);
- }
-
- // generate the spec, and specTexts with and without answers
- var specTextWithoutAnswers = "";
- var specTextWithAnswers = "";
- if (crossword.errors.length > 0) {
- specTextWithoutAnswers = crossword.errors.join("\n");
- } else {
- let specWithAnswers = generateSpec(crossword);
- crossword.spec = specWithAnswers;
- specTextWithAnswers = JSON.stringify(specWithAnswers);
-
- let specWithoutAnswers = generateSpec(crossword);
- delete specWithoutAnswers['answers'];
- specTextWithoutAnswers = JSON.stringify(specWithoutAnswers);
- }
- crossword.specTextWithAnswers = specTextWithAnswers;
- crossword.specTextWithoutAnswers = specTextWithoutAnswers;
-
- crossword.gridText = generateGridText( crossword );
-
- if (crossword.errors.length == 0) {
- console.log('parseWhateverItIs: no errors so generated DSLs');
- let withAnswers = true;
- crossword.DSLGeneratedFromDSLWithAnswers = generateDSL( crossword, withAnswers );
- crossword.DSLGeneratedFromDSLWithoutAnswers = generateDSL( crossword, ! withAnswers );
- } else {
- console.log( "parseWhateverItIs: errors found:\n", crossword.errors.join("\n") );
- }
-
- return crossword;
- }
-
- function parseWhateverItIsIntoSpecJson(text) {
- // returns spec or errors as JSON
- var crossword = parseWhateverItIs(text);
-
- var responseObj;
- if (crossword.errors.length == 0) {
- console.log("parseWhateverItIsIntoSpecJson: no errors found");
- responseObj = crossword.spec;
- } else {
- responseObj = {
- errors: crossword.errors,
- text : text
- }
- console.log("parseWhateverItIsIntoSpecJson: errors found:\n", crossword.errors.join("\n"), "\ntext=\n", text);
- }
-
- var jsonText = JSON.stringify( responseObj );
-
- return jsonText;
- }
-
- return {
- 'whateverItIs' : parseWhateverItIs,
- 'intoSpecJson' : parseWhateverItIsIntoSpecJson
- };
+ // given the DSL, ensure we have all the relevant pieces,
+ // and assume there will be subsequent checking to ensure they are valid
+ function parseDSL(text){
+ const crossword = {
+ version : "standard v1",
+ author : "",
+ editor : "Colin Inman",
+ publisher : "Financial Times",
+ copyright : "2017, Financial Times",
+ pubdate : "today",
+ dimensions : "17x17",
+ across : [],
+ down : [],
+ errors : [],
+ originalDSL : text,
+ };
+ let cluesGrouping;
+ const lines = text.split(/\r|\n/);
+
+ for(let line of lines){
+ // strip out comments
+ let match;
+ if (match = /^([^\#]*)\#.*$/.exec(line)) {
+ line = match[1];
+ }
+ // strip out trailing and leading spaces
+ line = line.trim();
+
+ if ( line === "" ) { /* ignore blank lines */ }
+ else if( line === "---") { /* ignore front matter lines */ }
+ else if (match = /^(layout|tag|tags|permalink):\s/ .exec(line) ) { /* ignore front matter fields */ }
+ else if (match = /^version:?\s+(.+)$/i .exec(line) ) { crossword.version = match[1]; }
+ else if (match = /^name:?\s+(.+)$/i .exec(line) ) { crossword.name = match[1]; }
+ else if (match = /^author:?\s+(.+)$/i .exec(line) ) { crossword.author = match[1]; }
+ else if (match = /^editor:?\s+(.+)$/i .exec(line) ) { crossword.editor = match[1]; }
+ else if (match = /^copyright:?\s+(.+)$/i .exec(line) ) { crossword.copyright = match[1]; }
+ else if (match = /^publisher:?\s+(.+)$/i .exec(line) ) { crossword.publisher = match[1]; }
+ else if (match = /^pubdate:?\s+(\d{4}\/\d\d\/\d\d)$/i.exec(line) ) { crossword.pubdate = match[1]; }
+ else if (match = /^(?:size|dimensions):?\s+(15x15|17x17)$/i.exec(line) ) { crossword.dimensions = match[1]; }
+ else if (match = /^(across|down):?$/i .exec(line) ) { cluesGrouping = match[1]; }
+ else if (match = /^-\s\((\d+),(\d+)\)\s+(\d+)\.\s+(.+)\s+\(([A-Z,\-*]+|[0-9,-]+)\)$/.exec(line) ) {
+ if (! /(across|down)/.test(cluesGrouping)) {
+ crossword.errors.push("ERROR: clue specified but no 'across' or 'down' grouping specified");
+ break;
+ } else {
+ const clue = {
+ coordinates : [ parseInt(match[1], 10), parseInt(match[2], 10) ],
+ id : parseInt(match[3], 10),
+ body : match[4],
+ answerCSV : match[5], // could be in the form of either "A,LIST-OF,WORDS" or "1,4-2,5"
+ original : line,
+ };
+ crossword[cluesGrouping].push(clue);
+ }
+ } else {
+ crossword.errors.push("ERROR: couldn't parse line: " + line);
+ }
+ }
+
+ return crossword;
+ }
+
+ // having found the pieces, check that they encode a valid crossword,
+ // creating useful data structures along the way
+ function validateAndEmbellishCrossword( crossword ){
+ const maxCoord = parseInt(crossword.dimensions.split('x')[0], 10);
+ crossword.maxCoord = maxCoord;
+ const grid = new Array( maxCoord * maxCoord ).fill(' ');
+ crossword.grid = grid;
+ const groupingPrev = {
+ across : {
+ id : 0,
+ x : 0,
+ y : 0
+ },
+ down : {
+ id : 0,
+ x : 0,
+ y : 0
+ }
+ };
+ const knownIds = {};
+ crossword.knownIds = knownIds;
+ let maxId = 0;
+
+ crossword.answers = {
+ across : [],
+ down : []
+ };
+
+ // insist on having at least one clue !
+ if ( crossword['across'].length + crossword['down'].length === 0) {
+ crossword.errors.push("Error: no valid clues specified");
+ }
+
+ function clueError(msg, clue, grouping){
+ crossword.errors.push("Error: " + msg + " in " + grouping + " clue=" + clue.original);
+ }
+
+ for(const grouping of ['across', 'down']){
+ const prev = groupingPrev[grouping];
+
+ for(const clue of crossword[grouping]){
+ // check non-zero id
+ if (clue.id === 0) {
+ clueError("id must be positive", clue, grouping);
+ break;
+ }
+
+ maxId = clue.id > maxId ? clue.id : maxId;
+
+ // check id sequence in order
+ if (clue.id <= prev.id) {
+ clueError("id out of sequence", clue, grouping);
+ break;
+ }
+
+ // check x,y within bounds
+ const x = clue.coordinates[0];
+ if (x > maxCoord) {
+ clueError("x coord too large", clue, grouping);
+ break;
+ }
+ const y = clue.coordinates[1];
+ if (y > maxCoord) {
+ clueError("y coord too large", clue, grouping);
+ break;
+ }
+
+ // check all clues with shared ids start at same coords
+ if (clue.id in knownIds) {
+ const knownCoords = knownIds[clue.id].coordinates;
+ if ( x !== knownCoords[0]
+ || y !== knownCoords[1]) {
+ clueError("shared id clashes with previous coordinates", clue, grouping);
+ break;
+ }
+ } else {
+ knownIds[clue.id] = clue;
+ }
+
+ {
+ // check answer within bounds
+ // and unpack the answerCSV
+
+ // convert "ANSWER,PARTS-INTO,NUMBERS" into number csv e.g. "6,5-4,6" (etc)
+ if ( /^[A-Z,\-*]+$/.test(clue.answerCSV) ) {
+ clue.numericCSV = clue.answerCSV.replace(/[A-Z*]+/g, match => {return match.length.toString(); } );
+ } else {
+ clue.numericCSV = clue.answerCSV;
+ }
+
+ // and if the answer is solely *s, replace that with the number csv
+ if ( /^[*,\-]+$/.test(clue.answerCSV) ) {
+ clue.answerCSV = clue.numericCSV;
+ }
+
+ const answerPieces = clue.answerCSV.split(/[,-]/);
+ const words = answerPieces.map(p => {
+ if (/^[0-9]+$/.test(p)) {
+ const pInt = parseInt(p, 10);
+ if (pInt === 0) {
+ clueError("answer contains a word size of 0", clue, grouping);
+ }
+ return '*'.repeat( pInt );
+ } else {
+ if (p.length === 0) {
+ clueError("answer contains an empty word", clue, grouping);
+ }
+ return p;
+ }
+ });
+
+ const wordsString = words.join('');
+ clue.wordsString = wordsString;
+ if (wordsString.length > maxCoord) {
+ clueError("answer too long for crossword", clue, grouping);
+ break;
+ }
+ crossword.answers[grouping].push(wordsString);
+
+ clue.wordsLengths = words.map(function(w){
+ return w.length;
+ });
+ }
+
+ // check answer + offset within bounds
+ if( grouping==='across' && clue.wordsString.length + x - 1 > maxCoord
+ || grouping==='down' && clue.wordsString.length + y - 1 > maxCoord ){
+ clueError("answer too long for crossword from that coord", clue, grouping);
+ break;
+ }
+
+ {
+ // check answer does not clash with previous answers
+ const step = grouping==='across'? 1 : maxCoord;
+ for (let i = 0; i < clue.wordsString.length; i++) {
+ const pos = x-1 + (y-1)*maxCoord + i*step;
+ if (grid[pos] === ' ') {
+ grid[pos] = clue.wordsString[i];
+ } else if( grid[pos] !== clue.wordsString[i] ) {
+ clueError("letter " + (i+1) + " clashes with previous clues", clue, grouping);
+ break;
+ }
+ }
+ }
+
+ // update prev
+ prev.id = clue.id;
+ prev.x = x;
+ prev.y = y;
+ }
+ }
+
+ // check we have a contiguous and complete clue id sequence
+ if (crossword.errors.length === 0) {
+ for (let i = 1; i <= maxId; i++) {
+ if (! (i in knownIds)) {
+ crossword.errors.push("Error: missing clue with id=" + i);
+ }
+ }
+ }
+
+ // check all the clues across and down are monotonic,
+ // i.e. each id starts to the right or down from the previous id
+ if (crossword.errors.length === 0) {
+ for (let i = 2; i <= maxId; i++) {
+ const prevClue = knownIds[i-1];
+ const clue = knownIds[i];
+
+ if ( clue.coordinates[0] + clue.coordinates[1] * maxCoord <= prevClue.coordinates[0] + prevClue.coordinates[1] * maxCoord ) {
+ if (clue.coordinates[1] < prevClue.coordinates[1]) {
+ crossword.errors.push("Error: clue " + clue.id + " starts above clue " + prevClue.id);
+ } else if (clue.coordinates[1] === prevClue.coordinates[1] && clue.coordinates[0] === prevClue.coordinates[0]) {
+ crossword.errors.push("Error: clue " + clue.id + " starts at same coords as clue " + prevClue.id);
+ } else {
+ crossword.errors.push("Error: clue " + clue.id + " starts to the left of clue " + prevClue.id);
+ }
+ break;
+ }
+ }
+ }
+
+ // check clues start from edge or from an empty cell
+
+ return crossword;
+ }
+
+ // a simple text display of the crossword answers in place
+ function generateGridText(crossword) {
+ let gridText = '';
+
+ if('grid' in crossword) {
+ const rows = [];
+ const maxCoord = crossword.maxCoord;
+ const grid = crossword.grid;
+
+ {
+ const row10s = [' ', ' ', ' '];
+ const row1s = [' ', ' ', ' '];
+ const rowSpaces = [' ', ' ', ' '];
+ for (let x = 1; x <= maxCoord; x++) {
+ const num10s = Math.floor(x/10);
+ row10s.push(num10s > 0? num10s : ' ');
+ row1s.push(x%10);
+ rowSpaces.push(' ');
+ }
+ rows.push(row10s.join(''));
+ rows.push(row1s.join(''));
+ rows.push(rowSpaces.join(''));
+ }
+
+ for (let y = 1; y <= maxCoord; y++) {
+ const row = [];
+ {
+ const num10s = Math.floor(y/10);
+ row.push(num10s > 0? num10s : ' ');
+ row.push(y%10);
+ row.push(' ');
+ }
+ for (let x = 1; x <= maxCoord; x++) {
+ let cell = grid[x-1 + (y-1)*maxCoord];
+ cell = cell === " "? '.' : cell;
+ row.push( cell );
+ }
+ rows.push( row.join('') );
+ }
+ gridText = rows.join("\n");
+ }
+
+ return gridText;
+ }
+
+ // having previously checked that the data encodes a valid crossword,
+ // actually construct the spec as a data structure,
+ // assuming a later step will convert it to JSON text
+ function generateSpec(crossword){
+ const spec = {
+ name : crossword.name,
+ author : crossword.author,
+ editor : crossword.editor,
+ copyright : crossword.copyright,
+ publisher : crossword.publisher,
+ date : crossword.pubdate,
+ size : {
+ rows : crossword.maxCoord,
+ cols : crossword.maxCoord,
+ },
+ grid : [],
+ gridnums : [],
+ clues : {
+ across : [],
+ down : [],
+ },
+ answers : crossword.answers,
+ notepad : "",
+ id : crossword.name,
+ };
+
+ // flesh out spec grid
+ for (let y = 1; y<=crossword.maxCoord; y++) {
+ const row = [];
+ for (let x = 1; x<=crossword.maxCoord; x++) {
+ const cell = crossword.grid[x-1 + (y-1)*crossword.maxCoord];
+ row.push( cell === ' '? '.' : 'X' );
+ }
+ spec.grid.push(row);
+ }
+
+ // flesh out gridnums
+ // fill with 0, then overwrite with ids
+
+ for (let y = 1; y<=crossword.maxCoord; y++) {
+ spec.gridnums.push( new Array(crossword.maxCoord).fill(0) );
+ }
+
+ for (const id in crossword.knownIds) {
+ if (Object.prototype.hasOwnProperty.call(crossword.knownIds, id)) {
+ const clue = crossword.knownIds[id];
+ spec.gridnums[clue.coordinates[1]-1][clue.coordinates[0]-1] = parseInt(id, 10);
+ }
+ }
+
+ // flesh out clues
+
+ ['across', 'down'].forEach( function(grouping){
+ crossword[grouping].forEach( function(clue) {
+ const item = [
+ parseInt(clue.id, 10),
+ clue.body + ' (' + clue.numericCSV + ')',
+ clue.wordsLengths,
+ clue.numericCSV
+ ];
+ spec.clues[grouping].push(item);
+ });
+ });
+
+ {
+ // if the answers are just placeholders (lots of *s or Xs)
+ // assume they are not to be displayed,
+ // so delete them from the spec
+ const concatAllAnswerWordsStrings = spec.answers.across.join('') + spec.answers.down.join('');
+ if ( /^(X+|\*+)$/.test(concatAllAnswerWordsStrings) ) {
+ delete spec['answers'];
+ }
+ }
+
+ return spec;
+ }
+
+ // given a crossword obj, generate the DSL for it
+ function generateDSL( crossword, withAnswers=true ){
+ let lines = [];
+ const nonClueFields = [
+ 'version', 'name', 'author', 'editor', 'copyright', 'publisher', 'pubdate',
+ ];
+ nonClueFields.forEach(field => {
+ lines.push(`${field}: ${crossword[field]}`);
+ });
+
+ lines.push(`size: ${crossword.dimensions}`);
+
+ ['across', 'down'].forEach( grouping => {
+ lines.push(`${grouping}:`);
+ crossword[grouping].forEach( clue => {
+ const pieces = [
+ '-',
+ `(${clue.coordinates.join(',')})`,
+ `${clue.id}.`,
+ clue.body,
+ `(${withAnswers? clue.answerCSV : clue.numericCSV})`
+ ];
+ lines.push(pieces.join(' '));
+ });
+ });
+
+ const footerComments = [
+ '',
+ "Notes on the text format...",
+ "Can't use square brackets or speech marks.",
+ "A clue has the form",
+ "- (COORDINATES) ID. Clue text (ANSWER)",
+ "Coordinates of clue in grid are (across,down), so (1,1) = top left, (17,17) = bottom right.",
+ "ID is a number, followed by a full stop.",
+ "(WORDS,IN,ANSWER): capitalised, and separated by commas or hyphens, or (numbers) separated by commas or hyphens.",
+ "ANSWERS with all words of ***** are converted to numbers.",
+ ];
+ lines = lines.concat( footerComments.map(c => { return `# ${c}`; } ) );
+
+ const frontMatterLine = '---';
+ lines.unshift( frontMatterLine );
+ lines.push ( frontMatterLine );
+
+ const dsl = lines.join("\n");
+
+ return dsl;
+ }
+
+ // given some text, decide what format it is (currently, only the DSL)
+ // and parse it accordingly,
+ // generating the grid text and output format if there are no errors,
+ // returning the crossword object with all the bits (or the errors).
+ function parseWhateverItIs(text) {
+ let crossword = parseDSL(text);
+
+ // only attempt to validate the crossword if no errors found so far
+ if (crossword.errors.length === 0) {
+ crossword = validateAndEmbellishCrossword(crossword);
+ console.log("parseWhateverItIs: validated crossword");
+ } else {
+ console.log("parseWhateverItIs: did not validate crossword=", crossword);
+ }
+
+ // generate the spec, and specTexts with and without answers
+ let specTextWithoutAnswers = "";
+ let specTextWithAnswers = "";
+ if (crossword.errors.length > 0) {
+ specTextWithoutAnswers = crossword.errors.join("\n");
+ } else {
+ const specWithAnswers = generateSpec(crossword);
+ crossword.spec = specWithAnswers;
+ specTextWithAnswers = JSON.stringify(specWithAnswers);
+
+ const specWithoutAnswers = generateSpec(crossword);
+ delete specWithoutAnswers['answers'];
+ specTextWithoutAnswers = JSON.stringify(specWithoutAnswers);
+ }
+ crossword.specTextWithAnswers = specTextWithAnswers;
+ crossword.specTextWithoutAnswers = specTextWithoutAnswers;
+
+ crossword.gridText = generateGridText( crossword );
+
+ if (crossword.errors.length === 0) {
+ console.log('parseWhateverItIs: no errors so generated DSLs');
+ const withAnswers = true;
+ crossword.DSLGeneratedFromDSLWithAnswers = generateDSL( crossword, withAnswers );
+ crossword.DSLGeneratedFromDSLWithoutAnswers = generateDSL( crossword, ! withAnswers );
+ } else {
+ console.log( "parseWhateverItIs: errors found:\n", crossword.errors.join("\n") );
+ }
+
+ return crossword;
+ }
+
+ function parseWhateverItIsIntoSpecJson(text) {
+ // returns spec or errors as JSON
+ const crossword = parseWhateverItIs(text);
+
+ let responseObj;
+ if (crossword.errors.length === 0) {
+ console.log("parseWhateverItIsIntoSpecJson: no errors found");
+ responseObj = crossword.spec;
+ } else {
+ responseObj = {
+ errors: crossword.errors,
+ text : text
+ };
+ console.log("parseWhateverItIsIntoSpecJson: errors found:\n", crossword.errors.join("\n"), "\ntext=\n", text);
+ }
+
+ const jsonText = JSON.stringify( responseObj );
+
+ return jsonText;
+ }
+
+ return {
+ 'whateverItIs' : parseWhateverItIs,
+ 'intoSpecJson' : parseWhateverItIsIntoSpecJson
+ };
}));
\ No newline at end of file
diff --git a/src/js/oCrossword.js b/src/js/oCrossword.js
index 924b200..2b9e195 100644
--- a/src/js/oCrossword.js
+++ b/src/js/oCrossword.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-inner-declarations */
/**
* Initialises an o-crossword components inside the element passed as the first parameter
*
@@ -13,7 +14,7 @@ function prevAll(node) {
const nodes = Array.from(node.parentNode.children);
const pos = nodes.indexOf(node);
return nodes.slice(0, pos);
-};
+}
function writeErrorsAsClues(rootEl, json) {
const cluesEl = rootEl.querySelector('ul.o-crossword-clues');
@@ -46,19 +47,19 @@ function writeErrorsAsClues(rootEl, json) {
function buildGrid(
rootEl,
-{
- size,
- name,
- gridnums,
- grid,
- clues,
- answers
-}) {
+ {
+ size,
+ name,
+ gridnums,
+ grid,
+ clues,
+ answers
+ }) {
const gridEl = rootEl.querySelector('table');
const cluesEl = rootEl.querySelector('ul.o-crossword-clues');
const {cols, rows} = size;
const emptyCell = rootEl.querySelector('.empty-fallback');
- let answerStore, isStorage;
+ let answerStore; let isStorage;
const cookie = 'FT-crossword_' + name.split(/[ ,]+/).join('');
expireStorage();
@@ -72,7 +73,7 @@ function buildGrid(
"across": [],
"down": [],
"timestamp": Date.now()
- }
+ };
isStorage = false;
}
@@ -99,7 +100,7 @@ function buildGrid(
}
rootEl.parentElement.setAttribute('data-o-crossword-title', name);
- rootEl.setAttribute('data-answer-version', !!answers);
+ rootEl.setAttribute('data-answer-version', Boolean(answers));
if (clues) {
rootEl.parentElement.setAttribute('data-o-crossword-clue-length', clues.across.length + clues.down.length);
@@ -127,26 +128,26 @@ function buildGrid(
tempPartial.classList.add('o-crossword-user-answer');
const answerLength = across[2].filter(isFinite).filter(isFinite).reduce((a,b)=>a+b,0);
- tempSpan.innerHTML = across[0] + 'across' + '. ' + across[1] + ' Press ENTER to complete your answer';
+ tempSpan.innerHTML = across[0] + 'across. ' + across[1] + ' Press ENTER to complete your answer';
tempLi.dataset.oCrosswordNumber = across[0];
tempLi.dataset.oCrosswordAnswerLength = answerLength;
tempLi.dataset.oCrosswordDirection = 'across';
tempLi.dataset.oCrosswordClueId = index;
- for(var i = 0; i < answerLength; ++i) {
- let tempInput = document.createElement('input');
+ for(let i = 0; i < answerLength; ++i) {
+ const tempInput = document.createElement('input');
tempInput.setAttribute('maxlength', 1);
tempInput.setAttribute('data-link-identifier', 'A' + across[0] + '-' + i);
tempInput.setAttribute('tabindex', -1);
if(answers) {
- let val = (answers.across[index][i] === '*')?'':answers.across[index][i];
+ const val = answers.across[index][i] === '*'?'':answers.across[index][i];
tempInput.value = val;
}
if(answerStore) {
if(isStorage) {
- let val = (answerStore.across[index][i] === '*')?'':answerStore.across[index][i];
+ const val = answerStore.across[index][i] === '*'?'':answerStore.across[index][i];
tempInput.value = val;
} else {
if(answerStore.across[index] === undefined) {
@@ -159,10 +160,10 @@ function buildGrid(
let count = 0;
if(across[3].length > 1) {
- for(var j = 0; j < across[3].length; ++j) {
+ for(let j = 0; j < across[3].length; ++j) {
if(j%2 === 1) {
- count += parseInt(across[3][j-1]);
- let separator = document.createElement('span');
+ count += parseInt(across[3][j-1], 10);
+ const separator = document.createElement('span');
separator.classList.add('separator');
if(across[3][j] === '-') {
@@ -170,7 +171,7 @@ function buildGrid(
} else if(across[3][j] === ',') {
separator.innerHTML = ' ';
}
-
+
if(i === count && separator.innerHTML !== '') {
tempPartial.appendChild(separator);
}
@@ -182,7 +183,7 @@ function buildGrid(
}
if(answerStore && !(/^[*,\-]+$/).test(answerStore.across[index])) {
- let srAnswer = answerStore.across[index];
+ const srAnswer = answerStore.across[index];
tempSpan.querySelector('.sr-answer').textContent = joinBlanks(srAnswer, 1);
}
@@ -199,26 +200,26 @@ function buildGrid(
tempPartial.classList.add('o-crossword-user-answer');
const answerLength = down[2].filter(isFinite).filter(isFinite).reduce((a,b)=>a+b,0);
- tempSpan.innerHTML = down[0] + 'down' + '. ' + down[1] + ' Press ENTER to complete your answer';
+ tempSpan.innerHTML = down[0] + 'down. ' + down[1] + ' Press ENTER to complete your answer';
tempLi.dataset.oCrosswordNumber = down[0];
tempLi.dataset.oCrosswordAnswerLength = answerLength;
tempLi.dataset.oCrosswordDirection = 'down';
tempLi.dataset.oCrosswordClueId = clues.across.length + index;
- for(var i = 0; i < answerLength; ++i) {
- let tempInput = document.createElement('input');
+ for(let i = 0; i < answerLength; ++i) {
+ const tempInput = document.createElement('input');
tempInput.setAttribute('maxlength', 1);
tempInput.setAttribute('data-link-identifier', 'D' + down[0] + '-' + i);
tempInput.setAttribute('tabindex', -1);
if(answers) {
- let val = (answers.down[index][i] === '*')?'':answers.down[index][i];
+ const val = answers.down[index][i] === '*'?'':answers.down[index][i];
tempInput.value = val;
}
if(answerStore) {
if(isStorage) {
- let val = (answerStore.down[index][i] === '*')?'':answerStore.down[index][i];
+ const val = answerStore.down[index][i] === '*'?'':answerStore.down[index][i];
tempInput.value = val;
} else {
if(answerStore.down[index] === undefined) {
@@ -231,10 +232,10 @@ function buildGrid(
let count = 0;
if(down[3].length > 1) {
- for(var j = 0; j < down[3].length; ++j) {
+ for(let j = 0; j < down[3].length; ++j) {
if(j%2 === 1) {
- count += parseInt(down[3][j-1]);
- let separator = document.createElement('span');
+ count += parseInt(down[3][j-1], 10);
+ const separator = document.createElement('span');
separator.classList.add('separator');
if(down[3][j] === '-') {
@@ -254,8 +255,8 @@ function buildGrid(
}
if(answerStore && !(/^[*,\-]+$/).test(answerStore.down[index])) {
- let srAnswer = answerStore.down[index];
- tempSpan.querySelector('.sr-answer').textContent = joinBlanks(srAnswer, 1);
+ const srAnswer = answerStore.down[index];
+ tempSpan.querySelector('.sr-answer').textContent = joinBlanks(srAnswer, 1);
}
downEl.appendChild(tempLi);
@@ -265,12 +266,12 @@ function buildGrid(
}
if (answers || answerStore) {
- let target = (answers)?answers:answerStore;
+ const target = answers?answers:answerStore;
clues.across.forEach(function acrossForEach(across, i) {
const answer = target.across[i];
const answerLength = answer.length;
getGridCellsByNumber(gridEl, across[0], 'across', answerLength).forEach((td, i) => {
- let val = (answer[i] === '*')?'':answer[i];
+ const val = answer[i] === '*'?'':answer[i];
td.textContent = val;
});
});
@@ -279,7 +280,7 @@ function buildGrid(
const answer = target.down[i];
const answerLength = answer.length;
getGridCellsByNumber(gridEl, down[0], 'down', answerLength).forEach((td, i) => {
- let val = (answer[i] === '*')?'':answer[i];
+ const val = answer[i] === '*'?'':answer[i];
td.textContent = val;
});
});
@@ -295,27 +296,19 @@ function expireStorage() {
const ts = Date.now();
for (let i = 0; i < localStorage.length; i++){
- if (localStorage.key(i).substring(0,12) == 'FT-crossword') {
- let storedItem = JSON.parse(localStorage.getItem(localStorage.key(i)));
- let difference = ts - storedItem.timestamp;
+ if (localStorage.key(i).substring(0,12) === 'FT-crossword') {
+ const storedItem = JSON.parse(localStorage.getItem(localStorage.key(i)));
+ const difference = ts - storedItem.timestamp;
- let daysCreated = difference/1000/60/60/24;
+ const daysCreated = difference/1000/60/60/24;
- if(daysCreated > 28) {
- localStorage.removeItem(localStorage.key(i));
- }
- }
+ if(daysCreated > 28) {
+ localStorage.removeItem(localStorage.key(i));
+ }
+ }
}
}
-function getRelativeCenter(e, el) {
- const bb = el.getBoundingClientRect();
- e.relativeCenter = {
- x: e.center.x - bb.left,
- y: e.center.y - bb.top,
- };
-}
-
function OCrossword(rootEl) {
if (!rootEl) {
rootEl = document.body;
@@ -335,7 +328,7 @@ function OCrossword(rootEl) {
- parse, generate data struct
- render
*/
- let p = new Promise( (resolve) => {
+ new Promise( (resolve) => {
if (this.rootEl.dataset.oCrosswordData.startsWith('http')) {
return fetch(this.rootEl.dataset.oCrosswordData)
.then(res => resolve(res.text()));
@@ -343,20 +336,20 @@ function OCrossword(rootEl) {
resolve( this.rootEl.dataset.oCrosswordData );
}
})
- .then(text => crosswordParser.intoSpecJson(text))
- .then(specText => JSON.parse(specText) )
- .then( json => {
- if (json.errors){
- console.log(`Found Errors after invoking crosswordParser.intoSpecJson:\n${json.errors.join("\n")}` );
- writeErrorsAsClues(rootEl, json);
- return Promise.reject("Failed to parse crossword data, so cannot generate crossword display");
- } else {
- return json;
- }
- })
- .then(json => buildGrid(rootEl, json))
- .then(() => this.assemble() )
- .catch( reason => console.log("Error caught in OCrossword: ", reason ) )
+ .then(text => crosswordParser.intoSpecJson(text))
+ .then(specText => JSON.parse(specText) )
+ .then( json => {
+ if (json.errors){
+ console.log(`Found Errors after invoking crosswordParser.intoSpecJson:\n${json.errors.join("\n")}` );
+ writeErrorsAsClues(rootEl, json);
+ return Promise.reject(new Error("Failed to parse crossword data, so cannot generate crossword display"));
+ } else {
+ return json;
+ }
+ })
+ .then(json => buildGrid(rootEl, json))
+ .then(() => this.assemble() )
+ .catch( reason => console.log("Error caught in OCrossword: ", reason ) )
;
}
}
@@ -370,19 +363,19 @@ function getGridCellsByNumber(gridEl, number, direction, length) {
if (direction === 'across') {
while (length--) {
out.push(el);
- if (length === 0) break;
+ if (length === 0) {break;}
el = el.nextElementSibling;
- if (!el) break;
+ if (!el) {break;}
}
}
else if (direction === 'down') {
const index = prevAll(el).length;
while (length--) {
out.push(el);
- if (length === 0) break;
- if (!el.parentNode.nextElementSibling) break;
+ if (length === 0) {break;}
+ if (!el.parentNode.nextElementSibling) {break;}
el = el.parentNode.nextElementSibling.children[index];
- if (!el) break;
+ if (!el) {break;}
}
}
}
@@ -391,15 +384,15 @@ function getGridCellsByNumber(gridEl, number, direction, length) {
}
function getLetterIndex(gridEl, cell, number, direction) {
- let el = gridEl.querySelector(`td[data-o-crossword-number="${number}"]`);
+ const el = gridEl.querySelector(`td[data-o-crossword-number="${number}"]`);
if(direction === 'across') {
return cell.cellIndex - el.cellIndex;
} else if (direction === 'down'){
- return parseInt(cell.parentNode.getAttribute('data-tr-index')) - parseInt(el.parentNode.getAttribute('data-tr-index'));
+ return parseInt(cell.parentNode.getAttribute('data-tr-index'), 10) - parseInt(el.parentNode.getAttribute('data-tr-index'), 10);
}
- return;
+
}
OCrossword.prototype.assemble = function assemble() {
@@ -421,16 +414,14 @@ OCrossword.prototype.assemble = function assemble() {
});
}
- let currentlySelectedGridItem = null;
- let answerStore = JSON.parse(this.rootEl.getAttribute('data-storage'));
+ let currentlySelectedGridItem = null;
+ const answerStore = JSON.parse(this.rootEl.getAttribute('data-storage'));
const isAnswerVersion = JSON.parse(this.rootEl.getAttribute('data-answer-version'));
if (cluesEl) {
let currentClue = -1;
- const cluesTotal = parseInt(this.rootEl.parentElement.getAttribute('data-o-crossword-clue-length')) - 1;
-
- const cluesUlEls = Array.from(cluesEl.querySelectorAll('ul'));
+ const cluesTotal = parseInt(this.rootEl.parentElement.getAttribute('data-o-crossword-clue-length'), 10) - 1;
const gridWrapper = document.createElement('div');
gridWrapper.classList.add('o-crossword-grid-wrapper');
@@ -499,10 +490,10 @@ OCrossword.prototype.assemble = function assemble() {
const buttonRow = document.createElement('div');
buttonRow.classList.add('o-crossword-button-row');
- this.rootEl.insertBefore(buttonRow, wrapper);
+ this.rootEl.insertBefore(buttonRow, wrapper);
const resetButton = document.createElement('button');
- resetButton.classList.add('o-crossword-reset', 'o-buttons', 'o-buttons--mono');
+ resetButton.classList.add('o-crossword-reset', 'o-buttons', 'o-crossword-button');
if(answersEmpty() || isAnswerVersion) {
resetButton.classList.add('hidden');
}
@@ -512,28 +503,28 @@ OCrossword.prototype.assemble = function assemble() {
buttonRow.appendChild(resetButton);
const toggleViewButtonAboveGrid = document.createElement('button');
- toggleViewButtonAboveGrid.classList.add('o-crossword-mobile-toggle', 'o-buttons', 'o-buttons--mono');
+ toggleViewButtonAboveGrid.classList.add('o-crossword-mobile-toggle', 'o-buttons', 'o-crossword-button');
toggleViewButtonAboveGrid.textContent = isGridView?'List view':'Grid view';
this.addEventListener(toggleViewButtonAboveGrid, 'click', toggleMobileViews);
this.rootEl.insertBefore(toggleViewButtonAboveGrid, gridWrapper);
const toggleViewButtonTop = document.createElement('button');
- toggleViewButtonTop.classList.add('o-crossword-mobile-toggle', 'o-buttons', 'o-buttons--mono');
+ toggleViewButtonTop.classList.add('o-crossword-mobile-toggle', 'o-buttons', 'o-crossword-button');
toggleViewButtonTop.textContent = isGridView?'List view':'Grid view';
this.addEventListener(toggleViewButtonTop, 'click', toggleMobileViews);
- buttonRow.appendChild(toggleViewButtonTop);
-
+ buttonRow.appendChild(toggleViewButtonTop);
+
const toggleColumnsButton = document.createElement('button');
- toggleColumnsButton.classList.add('o-crossword-mobile-toggle', 'o-buttons', 'o-buttons--mono');
+ toggleColumnsButton.classList.add('o-crossword-mobile-toggle', 'o-buttons', 'o-crossword-button');
toggleColumnsButton.textContent = isSingleColumnView?'2 col':'1 col';
this.addEventListener(toggleColumnsButton, 'click', toggleColumnView);
- buttonRow.appendChild(toggleColumnsButton);
+ buttonRow.appendChild(toggleColumnsButton);
const toggleViewButtonBottom = document.createElement('button');
- toggleViewButtonBottom.classList.add('o-crossword-mobile-toggle', 'o-buttons', 'o-buttons--mono');
+ toggleViewButtonBottom.classList.add('o-crossword-mobile-toggle', 'o-buttons', 'o-crossword-button');
toggleViewButtonBottom.textContent = isGridView?'List view':'Grid view';
this.addEventListener(toggleViewButtonBottom, 'click', toggleMobileViews);
@@ -603,13 +594,13 @@ OCrossword.prototype.assemble = function assemble() {
return;
}
- if((e.keyCode >= 65 && e.keyCode <= 90) || isAndroid()) {
+ if(e.keyCode >= 65 && e.keyCode <= 90 || isAndroid()) {
if(!isAndroid()) {
magicInput.value = String.fromCharCode(e.keyCode);
- let last = gridMap.get(magicInputTargetEl);
+ const last = gridMap.get(magicInputTargetEl);
Array.from(last).forEach(cell => {
- if(parseInt(cell.answerLength) - cell.answerPos === 1) {
+ if(parseInt(cell.answerLength, 10) - cell.answerPos === 1) {
e.target.select();
}
}); //a11y fix for screen reader
@@ -620,7 +611,7 @@ OCrossword.prototype.assemble = function assemble() {
progress();
} else {
- return;
+
}
});
@@ -649,12 +640,12 @@ OCrossword.prototype.assemble = function assemble() {
}
}
- let nextFocus = cluesEl.querySelector('li[data-o-crossword-clue-id="'+ currentClue +'"]');
+ const nextFocus = cluesEl.querySelector('li[data-o-crossword-clue-id="'+ currentClue +'"]');
nextFocus.focus();
}
if(e.keyCode === 13) {
- let inputs = e.target.querySelectorAll('input');
+ const inputs = e.target.querySelectorAll('input');
Array.from(inputs).forEach(input => {
input.setAttribute('tabindex', 1);
});
@@ -664,7 +655,7 @@ OCrossword.prototype.assemble = function assemble() {
return;
}
-
+
if(e.shiftKey && e.keyCode === 9) {
return nextInput(e.target, -1);
}
@@ -696,7 +687,7 @@ OCrossword.prototype.assemble = function assemble() {
nextInput(e.target, -1);
updateInBackground(e);
}, timer);
-
+
return;
}
@@ -707,14 +698,14 @@ OCrossword.prototype.assemble = function assemble() {
return;
}
- if((e.keyCode >= 65 && e.keyCode <= 90) || isAndroid()) {
+ if(e.keyCode >= 65 && e.keyCode <= 90 || isAndroid()) {
if(!isAndroid()) {
e.target.value = String.fromCharCode(e.keyCode);
}
e.target.select();
-
+
const identifier = e.target.getAttribute('data-link-identifier').split('-');
trackEvent({action: 'clueInput', clueId: identifier[0], letterId: identifier[1]});
@@ -723,9 +714,9 @@ OCrossword.prototype.assemble = function assemble() {
updateInBackground(e);
}, timer);
-
+
} else {
- return;
+
}
});
@@ -733,13 +724,13 @@ OCrossword.prototype.assemble = function assemble() {
getCellFromClue(e.target, gridSync => {
gridSync.grid.textContent = e.target.value;
- if(!!gridSync.defSync) {
- let defSync = cluesEl.querySelector('input[data-link-identifier="' + gridSync.defSyncInput +'"]');
+ if(gridSync.defSync) {
+ const defSync = cluesEl.querySelector('input[data-link-identifier="' + gridSync.defSyncInput +'"]');
defSync.value = e.target.value;
}
updateScreenReaderAnswer(e.target, gridSync);
- });
+ });
}
const progress = debounce(function progress(direction) {
@@ -776,8 +767,8 @@ OCrossword.prototype.assemble = function assemble() {
magicInput.value ='';
magicInput.style.display = 'none';
- let def = e.target.parentElement.parentElement;
- let targetClue = {
+ const def = e.target.parentElement.parentElement;
+ const targetClue = {
'number': def.getAttribute('data-o-crossword-number'),
'direction': def.getAttribute('data-o-crossword-direction'),
'answerLength': def.getAttribute('data-o-crossword-answer-length')
@@ -803,12 +794,12 @@ OCrossword.prototype.assemble = function assemble() {
const oldClue = currentlySelectedGridItem;
const clues = gridMap.get(el);
- if (!clues) return;
- currentlySelectedGridItem = clues.find(item => (
+ if (!clues) {return;}
+ currentlySelectedGridItem = clues.find(item =>
item.direction === oldClue.direction &&
item.number === oldClue.number &&
item.answerLength === oldClue.answerLength
- )) || currentlySelectedGridItem;
+ ) || currentlySelectedGridItem;
Array.from(gridEl.getElementsByClassName('receiving-input')).forEach(el => el.classList.remove('receiving-input'));
el.classList.add('receiving-input');
@@ -818,25 +809,25 @@ OCrossword.prototype.assemble = function assemble() {
magicInputNextEls = nextEls;
magicInput.style.left = magicInputTargetEl.offsetLeft + 'px';
magicInput.style.top = magicInputTargetEl.offsetTop + 'px';
-
+
magicInput.focus();
magicInput.select();
}
function nextInput(source, direction) {
- let inputID = source.getAttribute('data-link-identifier');
- let inputGroup = document.querySelectorAll('input[data-link-identifier^="' + inputID.split('-')[0] +'-"]');
+ const inputID = source.getAttribute('data-link-identifier');
+ const inputGroup = document.querySelectorAll('input[data-link-identifier^="' + inputID.split('-')[0] +'-"]');
let currentInput = inputID.split('-')[1];
- let newInput = (direction === 1)?++currentInput:--currentInput;
+ const newInput = direction === 1?++currentInput:--currentInput;
if(newInput >= 0 && newInput < inputGroup.length) {
- let next = cluesEl.querySelector('input[data-link-identifier="' + inputID.split('-')[0] +'-'+ newInput+'"]');
+ const next = cluesEl.querySelector('input[data-link-identifier="' + inputID.split('-')[0] +'-'+ newInput+'"]');
next.focus();
next.select();
} else {
source.blur();
- let def = source.parentElement.parentElement;
- let inputs = cluesEl.querySelectorAll('input');
+ const def = source.parentElement.parentElement;
+ const inputs = cluesEl.querySelectorAll('input');
Array.from(inputs).forEach(input => {
input.setAttribute('tabindex', -1);
});
@@ -850,7 +841,7 @@ OCrossword.prototype.assemble = function assemble() {
}
}
- let nextFocus = cluesEl.querySelector('li[data-o-crossword-clue-id="'+ currentClue +'"]');
+ const nextFocus = cluesEl.querySelector('li[data-o-crossword-clue-id="'+ currentClue +'"]');
nextFocus.focus();
} else {
@@ -883,29 +874,29 @@ OCrossword.prototype.assemble = function assemble() {
delete o.dataset.oCrosswordHighlighted;
}
const gridElsToHighlight = getGridCellsByNumber(gridEl, number, direction, length);
- gridElsToHighlight.forEach(el => el.dataset.oCrosswordHighlighted = direction);
+ gridElsToHighlight.forEach(el => {el.dataset.oCrosswordHighlighted = direction;});
}
function getCellFromClue(clue, callback) {
const inputIdentifier = clue.getAttribute('data-link-identifier');
- const defDirection = (inputIdentifier.slice(0,1) === 'A')?'across':'down';
+ const defDirection = inputIdentifier.slice(0,1) === 'A'?'across':'down';
const defNum = inputIdentifier.slice(1,inputIdentifier.length).split('-')[0];
- const defIndex = parseInt(inputIdentifier.split('-')[1]);
+ const defIndex = parseInt(inputIdentifier.split('-')[1], 10);
- let selectedCell = {};
+ const selectedCell = {};
for(const entry of gridMap) {
- let cellData = entry[1];
+ const cellData = entry[1];
for(let i = 0; i < cellData.length; ++i) {
if(
cellData[i].direction === defDirection &&
- parseInt(cellData[i].number) === parseInt(defNum) &&
- parseInt(cellData[i].answerPos) === parseInt(defIndex)
+ parseInt(cellData[i].number, 10) === parseInt(defNum, 10) &&
+ parseInt(cellData[i].answerPos, 10) === parseInt(defIndex, 10)
) {
selectedCell.grid = entry[0];
if(cellData.length > 1) {
selectedCell.defSyncInput = constructInputIdentifier(cellData, defDirection);
- selectedCell.defSync = (selectedCell.defSyncInput !== undefined);
+ selectedCell.defSync = selectedCell.defSyncInput !== undefined;
}
}
}
@@ -929,7 +920,7 @@ OCrossword.prototype.assemble = function assemble() {
});
el.classList.add('has-hover');
el.querySelector('.o-crossword-user-answer').style.top = clueDisplayerText.clientHeight + 'px';
- currentClue = parseInt(el.getAttribute('data-o-crossword-clue-id'));
+ currentClue = parseInt(el.getAttribute('data-o-crossword-clue-id'), 10);
if(isCSSMobile(clueDisplayer)) {
onResize(false);
@@ -979,10 +970,9 @@ OCrossword.prototype.assemble = function assemble() {
function updateScreenReaderAnswer(target, dataGrid) {
const targetData = target.parentNode.parentNode;
- const answerLength = parseInt(targetData.getAttribute('data-o-crossword-answer-length'));
const inputs = targetData.querySelectorAll('input');
const screenReaderAnswer = targetData.querySelector('.sr-answer');
- let answerValue = [];
+ const answerValue = [];
let filledCount = 0;
Array.from(inputs).forEach(input => {
@@ -996,8 +986,8 @@ OCrossword.prototype.assemble = function assemble() {
if(answerStore) {
const dir = targetData.getAttribute('data-o-crossword-direction');
- const offset = (dir === 'down')?cluesEl.querySelector('.o-crossword-clues-across').childElementCount:0;
- const targetIndex = parseInt(targetData.getAttribute('data-o-crossword-clue-id')) - offset;
+ const offset = dir === 'down'?cluesEl.querySelector('.o-crossword-clues-across').childElementCount:0;
+ const targetIndex = parseInt(targetData.getAttribute('data-o-crossword-clue-id'), 10) - offset;
answerStore[dir][targetIndex] = answerValue.join('');
saveLocal();
@@ -1010,19 +1000,19 @@ OCrossword.prototype.assemble = function assemble() {
}
screenReaderAnswer.textContent = joinBlanks(answerValue, filledCount);
-
+
if(dataGrid && dataGrid.defSync) {
- let syncTarget = cluesEl.querySelector('input[data-link-identifier=' + dataGrid.defSyncInput + ']');
+ const syncTarget = cluesEl.querySelector('input[data-link-identifier=' + dataGrid.defSyncInput + ']');
updateScreenReaderAnswer(syncTarget);
}
}
function syncPartialClue(letter, src, index) {
const gridItems = gridMap.get(src[index]);
- let targets = [];
+ const targets = [];
for(let i = 0; i < gridItems.length; ++i) {
- let linkName = gridItems[i].direction[0].toUpperCase() + gridItems[i].number + '-' + gridItems[i].answerPos;
+ const linkName = gridItems[i].direction[0].toUpperCase() + gridItems[i].number + '-' + gridItems[i].answerPos;
targets.push(cluesEl.querySelector('input[data-link-identifier="'+linkName+'"]'));
}
@@ -1034,19 +1024,19 @@ OCrossword.prototype.assemble = function assemble() {
const saveLocal = function saveLocal() {
try {
- let answerStoreID = this.rootEl.getAttribute('data-storage-id');
+ const answerStoreID = this.rootEl.getAttribute('data-storage-id');
localStorage.setItem(answerStoreID, JSON.stringify( answerStore ) );
} catch(err){
console.log('Error trying to save state', err);
}
}.bind(this);
- function clearAnswers(e) {
+ function clearAnswers() {
trackEvent({action: 'clearAnswers'});
resetButton.classList.add('hidden');
- let inputs = cluesEl.querySelectorAll('input');
- let cells = gridEl.querySelectorAll('td:not(.empty)');
+ const inputs = cluesEl.querySelectorAll('input');
+ const cells = gridEl.querySelectorAll('td:not(.empty)');
Array.from(inputs).forEach(input => {
input.value = '';
@@ -1057,29 +1047,29 @@ OCrossword.prototype.assemble = function assemble() {
});
try {
- let answerStoreID = this.parentElement.parentElement.getAttribute('data-storage-id');
+ const answerStoreID = this.parentElement.parentElement.getAttribute('data-storage-id');
localStorage.removeItem(answerStoreID);
} catch(err){
console.log('Error trying to save state', err);
}
}
- function toggleMobileViews(e) {
+ function toggleMobileViews() {
isGridView = !isGridView;
trackEvent({action: 'viewToggle', view: isGridView?'grid':'list'});
- let buttonText = isGridView?'List view':'Grid view';
- toggleViewButtonAboveGrid.textContent = buttonText;
+ const buttonText = isGridView?'List view':'Grid view';
+ toggleViewButtonAboveGrid.textContent = buttonText;
toggleViewButtonTop.textContent = buttonText;
toggleViewButtonBottom.textContent = buttonText;
if (isGridView) {
toggleColumnsButton.classList.add('visually_hidden');
- toggleViewButtonBottom.classList.add('visually_hidden');
+ toggleViewButtonBottom.classList.add('visually_hidden');
} else {
- toggleColumnsButton.classList.remove('visually_hidden');
- toggleViewButtonBottom.classList.remove('visually_hidden');
+ toggleColumnsButton.classList.remove('visually_hidden');
+ toggleViewButtonBottom.classList.remove('visually_hidden');
}
onResize(false);
@@ -1091,25 +1081,25 @@ OCrossword.prototype.assemble = function assemble() {
}
}
- function toggleColumnView(e) {
+ function toggleColumnView() {
isSingleColumnView = !isSingleColumnView;
- trackEvent({action: 'columnToggle', column: isSingleColumnView?'single':'double'})
+ trackEvent({action: 'columnToggle', column: isSingleColumnView?'single':'double'});
- let buttonText = isSingleColumnView?'2 col':'1 col';
+ const buttonText = isSingleColumnView?'2 col':'1 col';
toggleColumnsButton.textContent = buttonText;
if (isSingleColumnView) {
cluesEl.classList.add('o-crossword-clues-single-column');
- cluesEl.classList.remove('o-crossword-clues-two-columns');
+ cluesEl.classList.remove('o-crossword-clues-two-columns');
// o-crossword-clues-single-column
} else {
- cluesEl.classList.remove('o-crossword-clues-single-column');
- cluesEl.classList.add('o-crossword-clues-two-columns');
+ cluesEl.classList.remove('o-crossword-clues-single-column');
+ cluesEl.classList.add('o-crossword-clues-two-columns');
}
-
+
try {
- localStorage.setItem('FT-crossword_columns', isSingleColumnView);
+ localStorage.setItem('FT-crossword_columns', isSingleColumnView);
} catch(err){
console.log('Error trying to save state', err);
}
@@ -1121,7 +1111,7 @@ OCrossword.prototype.assemble = function assemble() {
const onResize = function onResize(init) {
const cellSizeMax = 40;
-
+
if (window.innerWidth <= 739) {
isMobile = true;
} else if (window.innerWidth > window.innerHeight && window.innerWidth <=739 ) { //rotated phones and small devices, but not iOS
@@ -1130,7 +1120,7 @@ OCrossword.prototype.assemble = function assemble() {
isMobile = false;
}
- if(isMobile && !!init) {
+ if(isMobile && Boolean(init)) {
clueNavigationNext.click();
}
@@ -1138,11 +1128,10 @@ OCrossword.prototype.assemble = function assemble() {
let d2 = gridEl.getBoundingClientRect();
const width1 = d1.width;
const height1 = d1.height;
- let width2 = d2.width;
const height2 = d2.height;
let scale = height2/height1;
- if (scale > 0.2) scale = 0.2;
+ if (scale > 0.2) {scale = 0.2;}
this._cluesElHeight = height1;
this._cluesElWidth = width1 * scale;
@@ -1154,22 +1143,22 @@ OCrossword.prototype.assemble = function assemble() {
//update grid size to fill 100% on mobile view
let fullWidth;
if (isAndroid()) {
- fullWidth = Math.min(window.screen.height, window.screen.width);
+ fullWidth = Math.min(window.screen.height, window.screen.width);
} else {
- fullWidth = Math.min(window.innerHeight, window.innerWidth);
+ fullWidth = Math.min(window.innerHeight, window.innerWidth);
}
-
+
this.rootEl.width = fullWidth + 'px !important';
const gridTDs = gridEl.querySelectorAll('td');
const gridSize = gridEl.querySelectorAll('tr').length;
- const newTdWidth = parseInt(fullWidth / (gridSize + 1) );
+ const newTdWidth = parseInt(fullWidth / (gridSize + 1), 10);
const inputEl = document.querySelector('.o-crossword-magic-input');
if(isMobile) {
for (let i = 0; i < gridTDs.length; i++) {
- let td = gridTDs[i];
+ const td = gridTDs[i];
td.style.width = Math.min(newTdWidth, cellSizeMax) + "px";
- td.style.height = Math.min(newTdWidth, cellSizeMax) + "px";
+ td.style.height = Math.min(newTdWidth, cellSizeMax) + "px";
td.style.maxWidth = "initial";
td.style.minWidth = "initial";
}
@@ -1180,8 +1169,8 @@ OCrossword.prototype.assemble = function assemble() {
if(isGridView) {
cluesEl.classList.add('visually_hidden');
- toggleViewButtonBottom.classList.add('visually_hidden');
- toggleColumnsButton.classList.add('visually_hidden');
+ toggleViewButtonBottom.classList.add('visually_hidden');
+ toggleColumnsButton.classList.add('visually_hidden');
gridWrapper.classList.remove('visually_hidden');
clueDisplayer.classList.remove('visually_hidden');
toggleViewButtonAboveGrid.classList.remove('visually_removed');
@@ -1191,18 +1180,18 @@ OCrossword.prototype.assemble = function assemble() {
toggleViewButtonAboveGrid.classList.add('visually_removed');
cluesEl.classList.remove('visually_hidden');
toggleViewButtonBottom.classList.remove('visually_hidden');
- toggleColumnsButton.classList.remove('visually_hidden');
+ toggleColumnsButton.classList.remove('visually_hidden');
}
if (isSingleColumnView) {
cluesEl.classList.add('o-crossword-clues-single-column');
- cluesEl.classList.remove('o-crossword-clues-two-columns');
+ cluesEl.classList.remove('o-crossword-clues-two-columns');
} else {
cluesEl.classList.remove('o-crossword-clues-single-column');
- cluesEl.classList.add('o-crossword-clues-two-columns');
+ cluesEl.classList.add('o-crossword-clues-two-columns');
}
- let el = cluesEl.querySelector('.has-hover');
+ const el = cluesEl.querySelector('.has-hover');
if(el) {
if(clueDisplayer.classList.contains('visually_hidden')) {
clueDisplayer.style.height = '';
@@ -1210,26 +1199,26 @@ OCrossword.prototype.assemble = function assemble() {
clueDisplayer.style.height = clueDisplayerText.clientHeight + 50 +'px';
el.querySelector('.o-crossword-user-answer').style.top = clueDisplayerText.clientHeight + 'px';
}
-
+
toggleViewButtonAboveGrid.style.marginTop = clueDisplayer.style.height;
}
} else {
for (let i = 0; i < gridTDs.length; i++) {
- let td = gridTDs[i];
+ const td = gridTDs[i];
td.style.removeProperty('width');
td.style.removeProperty('height');
td.style.removeProperty('max-width');
td.style.removeProperty('min-width');
}
- let desktopSize = gridTDs[0].getBoundingClientRect().width;
+ const desktopSize = gridTDs[0].getBoundingClientRect().width;
inputEl.style.width = desktopSize + "px";
inputEl.style.height = desktopSize + "px";
cluesEl.classList.add('o-crossword-clues-two-columns');
}
-
+
if(!isCSSMobile(clueDisplayer)){
gridEl.style.marginTop = "initial";
clueDisplayer.classList.remove('visually_hidden');
@@ -1261,7 +1250,7 @@ OCrossword.prototype.assemble = function assemble() {
defEl = navigateClues(e);
isNavigation = true;
} else {
- defEl = (e.target.nodeName === 'SPAN')?e.target.parentElement:e.target;
+ defEl = e.target.nodeName === 'SPAN'?e.target.parentElement:e.target;
}
clueDetails = {};
@@ -1276,7 +1265,7 @@ OCrossword.prototype.assemble = function assemble() {
if(!isMobile) {
defEl.focus();
}
-
+
target = gridEl.querySelector(`td[data-o-crossword-number="${clueDetails.number}"]`);
}
@@ -1310,23 +1299,23 @@ OCrossword.prototype.assemble = function assemble() {
const oldClue = currentlySelectedGridItem;
if(clueDetails !== undefined) {
- currentlySelectedGridItem = clues.find(item => (
+ currentlySelectedGridItem = clues.find(item =>
item.direction === clueDetails.direction &&
item.number === clueDetails.number &&
item.answerLength === clueDetails.answerLength
- ));
+ );
} else {
- currentlySelectedGridItem = clues.find(item => (
+ currentlySelectedGridItem = clues.find(item =>
item.direction === oldClue.direction &&
item.number === oldClue.number &&
item.answerLength === oldClue.answerLength
- ));
+ );
}
}
if (index !== -1 || !currentlySelectedGridItem) {
// the same cell has been clicked on again so
- if (index + 1 === clues.length) index = -1;
+ if (index + 1 === clues.length) {index = -1;}
currentlySelectedGridItem = clues[index + 1];
}
@@ -1351,13 +1340,13 @@ OCrossword.prototype.assemble = function assemble() {
if(target!== null) {
if(target.getAttribute('data-link-identifier')) {
const focus = target.getAttribute('data-link-identifier').split('-');
- trackEvent({action: 'focusClueInput', clueId: focus[0], letterId: focus[1]})
+ trackEvent({action: 'focusClueInput', clueId: focus[0], letterId: focus[1]});
} else{
const identifier = currentlySelectedGridItem.direction[0].toUpperCase() + currentlySelectedGridItem.number;
trackEvent({action: 'focusCell', clueId: identifier, letterId: currentlySelectedGridItem.answerPos});
}
}
- }.bind(this);
+ };
const navigateClues = function navigateClues (e) {
e.preventDefault();
@@ -1376,7 +1365,7 @@ OCrossword.prototype.assemble = function assemble() {
}
return cluesEl.querySelector(`li[data-o-crossword-clue-id="${currentClue}"]`);
- }.bind(this);
+ };
this.addEventListener(cluesEl, 'mousemove', e => highlightGridByCluesEl(e.target));
@@ -1424,12 +1413,12 @@ OCrossword.prototype.destroy = function destroy() {
module.exports = OCrossword;
function isiOS() {
- var iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+ const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
return iOS;
}
function isAndroid() {
- var android = navigator.userAgent.toLowerCase().indexOf("android") > -1;
+ const android = navigator.userAgent.toLowerCase().indexOf("android") > -1;
return android;
}
@@ -1442,34 +1431,34 @@ function isCSSMobile(clueDisplayer) {
}
function isEquivalent(a, b) {
- var aProps = Object.getOwnPropertyNames(a);
- var bProps = Object.getOwnPropertyNames(b);
+ const aProps = Object.getOwnPropertyNames(a);
+ const bProps = Object.getOwnPropertyNames(b);
- if (aProps.length != bProps.length) {
- return false;
- }
+ if (aProps.length !== bProps.length) {
+ return false;
+ }
- for (var i = 0; i < aProps.length; i++) {
- var propName = aProps[i];
- if (a[propName] !== b[propName]) {
- return false;
- }
- }
+ for (let i = 0; i < aProps.length; i++) {
+ const propName = aProps[i];
+ if (a[propName] !== b[propName]) {
+ return false;
+ }
+ }
- return true;
+ return true;
}
function initTracking(id, view, column) {
const config_data = {
- server: 'https://spoor-api.ft.com/px.gif',
- context: {
- product: 'o-crossword',
- crosswordNumber: id
- },
- user: {
- ft_session: oTracking.utils.getValueFromCookie(/FTSession=([^;]+)/)
- }
- }
+ server: 'https://spoor-api.ft.com/px.gif',
+ context: {
+ product: 'o-crossword',
+ crosswordNumber: id
+ },
+ user: {
+ ft_session: oTracking.utils.getValueFromCookie(/FTSession=([^;]+)/)
+ }
+ };
oTracking.init(config_data);
oTracking.page({
@@ -1494,19 +1483,19 @@ function trackEvent(action) {
function joinBlanks (answerValue, filledCount) {
let combineCount = 0;
- let combinedValue = [];
+ const combinedValue = [];
for(let i = 0; i < answerValue.length; ++i) {
if(answerValue[i] === '*') {
++combineCount;
- if((i < answerValue.length - 1 && answerValue[i + 1] !== '*') || i === answerValue.length - 1) {
+ if(i < answerValue.length - 1 && answerValue[i + 1] !== '*' || i === answerValue.length - 1) {
if(combineCount > 1) {
combinedValue.push(" " + combineCount + " blanks ");
} else {
combinedValue.push(" blank ");
}
}
- } else {
+ } else {
combineCount = 0;
combinedValue.push(answerValue[i]);
}
@@ -1515,6 +1504,6 @@ function joinBlanks (answerValue, filledCount) {
if(filledCount > 0) {
return 'Your Answer: ' + combinedValue.join('') + '.';
}
-
+
return '';
}
\ No newline at end of file
diff --git a/src/scss/_base.scss b/src/scss/_base.scss
index faa56d2..b58c0b5 100644
--- a/src/scss/_base.scss
+++ b/src/scss/_base.scss
@@ -3,10 +3,11 @@
/// @link http://registry.origami.ft.com/components/o-crossword
////
+// sass-lint:disable no-important, no-qualifying-elements, mixins-before-declarations, no-duplicate-properties
@mixin oCrosswordType {
font-family: monospace;
min-width: 1.5em;
- min-width: 2ch;
+ min-width: 2ch;
max-width: 1.5em;
max-width: 2ch;
height: 1.5em;
@@ -46,7 +47,8 @@
flex-flow: wrap;
align-content: flex-start;
- .o-crossword-clues-across, .o-crossword-clues-down {
+ .o-crossword-clues-across,
+ .o-crossword-clues-down {
display: block;
}
}
@@ -56,6 +58,10 @@
box-sizing: border-box;
}
+ .o-crossword-button {
+ @include oButtons($theme: "mono");
+ }
+
.hidden {
display: none;
}
@@ -80,10 +86,10 @@
text-transform: none;
margin: 0 auto;
display: table;
- box-shadow: 2px 0 1px rgba(0,0,0,.5);
+ box-shadow: 2px 0 1px rgba(0, 0, 0, 0.5);
position: fixed;
- top:0;
+ top: 0;
left: 0;
background: white;
width: 100%;
@@ -161,13 +167,13 @@
top: 0;
left: 0;
margin: 0;
- border: none;
+ border: 0;
outline: none;
box-sizing: border-box;
- background: rgba(0,0,0,0.2);
+ background: rgba(0, 0, 0, 0.2);
font-size: 1.5rem;
text-transform: inherit;
- max-width: none!important;
+ max-width: none !important;
border-radius: 0;
padding: 0;
}
@@ -209,11 +215,11 @@
}
&[data-o-crossword-highlighted="across"] {
- background-color:rgba(158, 47, 80,.5);
+ background-color: rgba(158, 47, 80, 0.5);
}
&[data-o-crossword-highlighted="down"] {
- background-color:rgba(158, 47, 80,.5);
+ background-color: rgba(158, 47, 80, 0.5);
}
text-align: center;
@@ -228,7 +234,7 @@
left: 0;
letter-spacing: 0;
line-height: 1em;
- background: rgba(255,255,255,0.8);
+ background: rgba(255, 255, 255, 0.8);
}
}
}
@@ -250,7 +256,8 @@
float: left;
display: flex;
- &, ul {
+ &,
+ ul {
list-style: none;
margin: 0;
padding: 0;
@@ -263,7 +270,7 @@
cursor: pointer;
display: block;
- .has-hover > span{
+ .has-hover > span {
@include oColorsFor(o-crossword-clue-highlight, text);
}
@@ -282,7 +289,7 @@
&:focus {
outline: none;
- & > span{
+ & > span {
@include oColorsFor(o-crossword-clue-highlight, text);
}
}
@@ -293,9 +300,9 @@
margin-top: 0.5em;
input {
- border:0;
+ border: 0;
display: inline-block;
- border-bottom: 1px solid #000;
+ border-bottom: 1px solid #000000;
margin-right: 2px;
width: 25px;
height: 25px;
@@ -351,7 +358,7 @@
height: auto;
margin-left: 0.9em !important;
box-shadow: none;
- border: none;
+ border: 0;
transition: none;
pointer-events: all;
transform: none !important;
diff --git a/src/scss/_mixins.scss b/src/scss/_mixins.scss
index 95eef7b..006e661 100644
--- a/src/scss/_mixins.scss
+++ b/src/scss/_mixins.scss
@@ -11,4 +11,8 @@
.#{$classname} {
@include oCrosswordBase;
}
-}
\ No newline at end of file
+ @include _oCrosswordOrientation;
+ @include _oCrosswordPrint;
+ @include _oCrosswordMobile;
+
+}
diff --git a/src/scss/_mobile.scss b/src/scss/_mobile.scss
index 8729103..a1bd06d 100644
--- a/src/scss/_mobile.scss
+++ b/src/scss/_mobile.scss
@@ -1,5 +1,8 @@
-@media screen and (max-width: $break-phone) {
- .o-crossword table, .o-crossword .o-crossword-grid-wrapper .o-crossword-magic-input {
+// sass-lint:disable no-vendor-prefixes
+@mixin _oCrosswordMobile () {
+ @media screen and (max-width: $break-phone) {
+ .o-crossword table,
+ .o-crossword .o-crossword-grid-wrapper .o-crossword-magic-input {
font-size: 1.2rem;
}
@@ -24,7 +27,7 @@
.o-crossword .o-crossword-clue-displayer {
height: 3.6em;
- font-size: .85rem;
+ font-size: 0.85rem;
padding: 0 0.5em 1em;
span {
@@ -55,4 +58,5 @@
}
}
}
+ }
}
diff --git a/src/scss/_orientation.scss b/src/scss/_orientation.scss
index 25aa6d8..7474681 100644
--- a/src/scss/_orientation.scss
+++ b/src/scss/_orientation.scss
@@ -1,11 +1,14 @@
+// sass-lint:disable no-ids
//below forces portrait mode on mobile phones
//it uses "min-aspect-ratio" to detect landscape,
//since keyboard popping will make "orientation" wrong in some cases
-@media screen and (min-aspect-ratio: 13/9) and (max-width: $break-phone) {
- body:not(.iOS) #main-container {
- transform: rotate(-90deg);
- width: 100% /* screen width */ ;
- height: 100% /* screen height */ ;
- overflow: scroll;
+@mixin _oCrosswordOrientation () {
+ @media screen and (min-aspect-ratio: 13/9) and (max-width: $break-phone) {
+ body:not(.iOS) #main-container {
+ transform: rotate(-90deg);
+ width: 100% /* screen width */ ;
+ height: 100% /* screen height */ ;
+ overflow: scroll;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/scss/_print.scss b/src/scss/_print.scss
index 6dedd27..d70471e 100644
--- a/src/scss/_print.scss
+++ b/src/scss/_print.scss
@@ -1,6 +1,8 @@
-@media print {
+// sass-lint:disable no-qualifying-elements, no-important, no-vendor-prefixes
+@mixin _oCrosswordPrint () {
+ @media print {
@page {
- margin: 1cm 0.5cm 1cm 0.5cm;
+ margin: 1cm 0.5cm;
}
body {
-webkit-print-color-adjust: exact;
@@ -8,7 +10,7 @@
& >div {
height: auto;
- page-break-after:avoid;
+ page-break-after: avoid;
position: relative;
width: 100%;
@@ -16,40 +18,40 @@
content: attr(data-o-crossword-title);
display: block;
text-align: center;
- margin: .5cm 0 0;
+ margin: 0.5cm 0 0;
text-transform: capitalize;
}
}
}
.o-crossword {
- padding: 0!important;
+ padding: 0 !important;
display: block;
- background-color: white!important;
+ background-color: white !important;
}
.o-crossword table {
- margin-top: 0!important;
+ margin-top: 0 !important;
}
.o-crossword table td {
- min-width: 0!important;
- width: 25px!important;
- height: 25px!important;
+ min-width: 0 !important;
+ width: 25px !important;
+ height: 25px !important;
}
.o-crossword table td[data-o-crossword-highlighted="across"],
.o-crossword table td[data-o-crossword-highlighted="down"] {
- background-color: transparent!important;
+ background-color: transparent !important;
}
.o-crossword-clue-displayer {
- display: none!important;
+ display: none !important;
}
.o-crossword table td[data-o-crossword-number]:before {
- font-size: 8px!important;
- background: transparent!important;
+ font-size: 8px !important;
+ background: transparent !important;
}
.o-crossword .o-crossword-grid-wrapper {
@@ -58,45 +60,45 @@
}
.o-crossword .o-crossword-grid-wrapper .o-crossword-magic-input {
- background: transparent!important;
+ background: transparent !important;
}
.o-crossword .o-crossword-mobile-toggle,
.o-crossword .o-crossword-reset {
- display: none!important;
+ display: none !important;
}
.o-crossword .o-crossword-clues,
.o-crossword .o-crossword-clues ul {
- padding-bottom: 0!important;
+ padding-bottom: 0 !important;
}
.o-crossword .o-crossword-clues,
.o-crossword .o-crossword-clues.visually_hidden {
- width: 100%!important;
- max-width: none!important;
- display: block!important;
- height: auto!important;
+ width: 100% !important;
+ max-width: none !important;
+ display: block !important;
+ height: auto !important;
}
.o-crossword .o-crossword-clues li.has-hover,
.o-crossword .o-crossword-clues li.has-hover span,
.o-crossword .o-crossword-clues ul li.has-hover,
.o-crossword .o-crossword-clues ul li.has-hover span {
- color: black!important;
+ color: black !important;
}
.o-crossword .o-crossword-clues li,
.o-crossword .o-crossword-clues li span,
.o-crossword .o-crossword-clues ul li,
.o-crossword .o-crossword-clues ul li span {
- line-height: 1.4!important;
- font-size: 14px!important;
+ line-height: 1.4 !important;
+ font-size: 14px !important;
}
.o-crossword .o-crossword-clues ul li .o-crossword-user-answer,
.o-crossword .o-crossword-clues ul .o-crossword-user-answer {
- display: none!important;
+ display: none !important;
}
.o-crossword:not([data-o-crossword-force-compact]) {
@@ -111,21 +113,22 @@
display: block;
>li {
- padding: 0!important;
+ padding: 0 !important;
}
- .o-crossword-clues-across, .o-crossword-clues-down {
+ .o-crossword-clues-across,
+ .o-crossword-clues-down {
display: inline-block;
float: left;
width: 48%;
margin-top: 1cm;
- vertical-align: top!important;
+ vertical-align: top !important;
&:before {
- font-size: 28px!important;
+ font-size: 28px !important;
display: block;
padding: 0;
- margin-bottom: 10px!important;
+ margin-bottom: 10px !important;
}
}
@@ -138,4 +141,5 @@
.o-crossword-reset {
display: none;
}
+ }
}
diff --git a/test/helpers/sandbox.js b/test/helpers/sandbox.js
deleted file mode 100644
index 39e093b..0000000
--- a/test/helpers/sandbox.js
+++ /dev/null
@@ -1,21 +0,0 @@
-let sandboxEl;
-
-export function init() {
- if (document.querySelector('.sandbox')) {
- sandboxEl = document.querySelector('.sandbox');
- } else {
- sandboxEl = document.createElement('div');
- sandboxEl.classList.add('sandbox');
- document.body.appendChild(sandboxEl);
- }
-}
-
-export function reset() {
- while (sandboxEl.firstChild) {
- sandboxEl.removeChild(sandboxEl.firstChild);
- }
-}
-
-export function setContents(html) {
- sandboxEl.innerHTML = html;
-}