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; -}