diff --git a/src/js/crossword_parser.js b/src/js/crossword_parser.js index e45504f..4b18c89 100644 --- a/src/js/crossword_parser.js +++ b/src/js/crossword_parser.js @@ -51,7 +51,7 @@ 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) ) { + 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; @@ -157,14 +157,14 @@ // 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() } ); + 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 Xs, replace that with the number csv - if ( /^[X,\-]+$/.test(clue.answerCSV) ) { + // and if the answer is solely *s, replace that with the number csv + if ( /^[*,\-]+$/.test(clue.answerCSV) ) { clue.answerCSV = clue.numericCSV; } @@ -175,7 +175,7 @@ if (pInt == 0) { clueError("answer contains a word size of 0"); } - return 'X'.repeat( pInt ); + return '*'.repeat( pInt ); } else { if (p.length == 0) { clueError("answer contains an empty word"); @@ -239,7 +239,7 @@ if (crossword.errors.length == 0) { for (var i = 2; i <= maxId; i++) { let prevClue = knownIds[i-1]; - let clue = knownIds[i]; + 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]) { @@ -375,11 +375,11 @@ }); { - // if the answers are just placeholders (lots of Xs) + // 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) ) { + if ( /^(X+|\*+)$/.test(concatAllAnswerWordsStrings) ) { delete spec['answers']; } } @@ -422,7 +422,7 @@ "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 XXXXXX are converted to numbers.", + "ANSWERS with all words of ***** are converted to numbers.", ]; lines = lines.concat( footerComments.map(c => { return `# ${c}`; } ) ); @@ -506,4 +506,4 @@ 'whateverItIs' : parseWhateverItIs, 'intoSpecJson' : parseWhateverItIsIntoSpecJson }; -})); +})); \ No newline at end of file diff --git a/src/js/oCrossword.js b/src/js/oCrossword.js index d0bc4ca..2c541e7 100644 --- a/src/js/oCrossword.js +++ b/src/js/oCrossword.js @@ -57,6 +57,25 @@ function buildGrid( const cluesEl = rootEl.querySelector('ul.o-crossword-clues'); const {cols, rows} = size; const emptyCell = rootEl.querySelector('.empty-fallback'); + let answerStore, isStorage; + const cookie = 'FT-crossword_' + name.split(/[ ,]+/).join(''); + + expireStorage(); + + if(!answers) { + if(localStorage.getItem(cookie)) { + answerStore = JSON.parse(localStorage.getItem(cookie)); + isStorage = true; + } else { + answerStore = { + "across": [], + "down": [], + "timestamp": Date.now() + } + + isStorage = false; + } + } for (let i=0; i { - td.textContent = answer[i]; + let val = (answer[i] === '*')?'':answer[i]; + td.textContent = val; }); }); clues.down.forEach(function downForEach(down, i) { - const answer = answers.down[i]; + const answer = target.down[i]; const answerLength = answer.length; getGridCellsByNumber(gridEl, down[0], 'down', answerLength).forEach((td, i) => { - td.textContent = answer[i]; + let val = (answer[i] === '*')?'':answer[i]; + td.textContent = val; }); }); } + + if(answerStore) { + rootEl.setAttribute('data-storage', JSON.stringify(answerStore)); + rootEl.setAttribute('data-storage-id', cookie); + } +} + +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; + + let daysCreated = difference/1000/60/60/24; + + if(daysCreated > 28) { + localStorage.removeItem(localStorage.key(i)); + } + } + } } function getRelativeCenter(e, el) { @@ -319,6 +388,7 @@ function getLetterIndex(gridEl, cell, number, direction) { OCrossword.prototype.assemble = function assemble() { const gridEl = this.rootEl.querySelector('table'); const cluesEl = this.rootEl.querySelector('ul.o-crossword-clues'); + let answerStore = JSON.parse(this.rootEl.getAttribute('data-storage')); const gridMap = new Map(); let currentlySelectedGridItem = null; for (const el of cluesEl.querySelectorAll('[data-o-crossword-number]')) { @@ -391,6 +461,16 @@ OCrossword.prototype.assemble = function assemble() { let previousClueSelection = null; let isTab = false; + const resetButton = document.createElement('button'); + resetButton.classList.add('o-crossword-reset'); + if(answersEmpty()) { + resetButton.classList.add('hidden'); + } + resetButton.textContent = 'Reset grid'; + + this.addEventListener(resetButton, 'click', clearAnswers); + this.rootEl.insertBefore(resetButton, gridWrapper); + function constructInputIdentifier(data, direction) { let identifier; @@ -549,7 +629,7 @@ OCrossword.prototype.assemble = function assemble() { defSync.value = e.target.value; } - updateScreenReaderAnswer(e.target); + updateScreenReaderAnswer(e.target, gridSync); nextInput(e.target, -1); }, timer); @@ -806,17 +886,32 @@ OCrossword.prototype.assemble = function assemble() { ++filledCount; answerValue.push(input.value); } else { - answerValue.push("."); + answerValue.push("*"); } }); + 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; + answerStore[dir][targetIndex] = answerValue.join(''); + + saveLocal(); + + if(answersEmpty()) { + resetButton.classList.add('hidden'); + } else { + resetButton.classList.remove('hidden'); + } + } + let combineCount = 0; let combinedValue = []; for(let i = 0; i < answerValue.length; ++i) { - if(answerValue[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 { @@ -856,6 +951,40 @@ OCrossword.prototype.assemble = function assemble() { }); } + const saveLocal = function saveLocal() { + try { + let 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) { + resetButton.classList.add('hidden'); + let inputs = cluesEl.querySelectorAll('input'); + let cells = gridEl.querySelectorAll('td:not(.empty)'); + + Array.from(inputs).forEach(input => { + input.value = ''; + }); + + Array.from(cells).forEach(cell => { + cell.textContent = ''; + }); + + try { + let answerStoreID = this.parentElement.getAttribute('data-storage-id'); + localStorage.removeItem(answerStoreID); + } catch(err){ + console.log('Error trying to save state', err); + } + } + + function answersEmpty() { + return (/^[*,\-]+$/).test(answerStore.across) && (/^[*,\-]+$/).test(answerStore.down); + } + const onResize = function onResize(init) { var isMobile = false; const cellSizeMax = 40; diff --git a/src/scss/_base.scss b/src/scss/_base.scss index d8ed261..adfd29e 100644 --- a/src/scss/_base.scss +++ b/src/scss/_base.scss @@ -305,5 +305,17 @@ } } } + + .o-crossword-reset { + margin: 0 10px 20px; + + @include oGridRespondTo(M) { + margin: 0; + right: 10px; + top: 10px; + position: absolute; + z-index: 2; + } + } } diff --git a/src/scss/_print.scss b/src/scss/_print.scss index 97b569a..37a2ac4 100644 --- a/src/scss/_print.scss +++ b/src/scss/_print.scss @@ -122,4 +122,8 @@ } } } + + .o-crossword-reset { + display: none; + } } \ No newline at end of file