'use strict' // Common UI code for anagram games that involve entering letters. function clearElement(element) { // From https://stackoverflow.com/a/3955238 while (element.firstChild) { element.removeChild(element.firstChild); } } class AnagramEntryUI { constructor() { this.loadSettings(); let ui = this; document.getElementById('submit') .addEventListener('click', _ => ui.submitWord()); document.getElementById('clear') .addEventListener('click', _ => ui.clearAll()); document.getElementById('clear_nonlocked') .addEventListener('click', _ => ui.clearUnlocked()); document.getElementById('newgame') .addEventListener('click', _ => ui.nextGame()); document.getElementById('switch_game') .addEventListener('click', _ => { let fragment = document.getElementById('available_games').value; if (!ui.game || fragment != ui.game.fragment) { ui.nextGame(ui.gameClass.fromFragment(fragment)); } }); document.addEventListener('keyup', event => { if (!ui.game || document.activeElement.nodeName.toUpperCase() === 'INPUT') { return; } if (event.key === "Enter") { ui.submitWord(); } else if (event.key === "Backspace") { let lastUnlocked = ui.game.numLetters - 1; while (lastUnlocked >= 0 && ui.locks[lastUnlocked].checked) { lastUnlocked--; } if (ui.focusedTextbox < lastUnlocked || ui.letters[lastUnlocked] == '') { ui.focusPreviousTextbox(); } ui.clearCurrent(); } else if (event.key === "Clear" || event.key == "Delete") { ui.clearCurrent(); } else if (event.key === "ArrowLeft") { ui.focusPreviousTextbox(); } else if (event.key === "ArrowRight" || event.key === "Space") { ui.focusNextTextbox(); } else if (event.key === "Home") { ui.setFocusedTextboxIndex(0); } else if (event.key === "End") { ui.setFocusedTextboxIndex(ui.game.numLetters - 1); } else if (event.key === "/") { let lock = ui.locks[ui.focusedTextbox]; lock.checked = !lock.checked; lock.dispatchEvent(new Event('change')); } else if (event.key === "?") { ui.locks.forEach(lock => lock.checked = false); } else if (ui.game.availableLetters.includes(event.key)) { ui.enterLetter(event.key); } }); document.getElementById('copy_permalink') .addEventListener('click', _ => { let permalink = document.getElementById('permalink_input'); permalink.focus(); let range = document.createRange(); range.selectNodeContents(permalink); let s = window.getSelection(); s.removeAllRanges(); s.addRange(range); permalink.setSelectionRange(0, permalink.value.length); document.execCommand('copy'); permalink.blur(); }); document.getElementById('newgame_button') .addEventListener('click', _ => { if (this.getSettingsObjectFromForm() === null) return; ui.newGame(); }); } initializeInputs(len) { let ui = this; let shuffle = document.getElementById('shuffle'); // Clear event listeners. From https://stackoverflow.com/a/19470348 let newShuffle = shuffle.cloneNode(true) shuffle.parentNode.replaceChild(newShuffle, shuffle); newShuffle.addEventListener('click', function() { ui.shuffleAvailableLetters(); }); this.autoSubmit = document.getElementById('auto_submit'); this.autoSubmit.addEventListener('change', _ => ui.saveSettings()); let entry = document.getElementById('letters_entry'); clearElement(entry); this.textboxes = new Array(len); this.letters = new Array(len); this.letters.fill(''); this.locks = new Array(len); this.focusedTextbox = 0; for (let i=0; i < len; i++) { let t = document.getElementById('letter_entry'); let clone = document.importNode(t.content, true); let lock_checkbox = clone.querySelector('input.lock'); this.locks[i] = lock_checkbox; lock_checkbox.id = 'lock' + i; lock_checkbox.addEventListener('change', e => { if (ui.locks[i].checked && ui.letters[i] == '' && ui.game.knownMatches !== undefined && ui.game.knownMatches.has(i)) { let letter = ''; Object.values(ui.game.knownInformation).forEach(info => { if (info.knownMatches.has(i)) { letter = info.letter; } }); ui.textboxes[i].innerText = letter; ui.letters[i] = letter; } if (ui.focusedTextbox == i || ui.locks[ui.focusedTextbox].checked) { ui.setFocusedTextboxIndex(i); } }); clone.querySelector('label.lock_label').htmlFor = 'lock' + i; let textbox = clone.querySelector('span.letter'); this.textboxes[i] = textbox; textbox.parentNode.addEventListener('click', e => { ui.setFocusedTextboxIndex(i); }); entry.appendChild(clone); } this.shuffleAvailableLetters(); ui.setFocusedTextboxIndex(0); document.getElementById('letters_entry').style.display = ''; document.getElementById('submit_buttons').style.display = ''; document.getElementById('endgame').style.display = 'none'; } recolorAvailableLetters() { // To be implemented by subclass. } shuffleAvailableLetters() { let ui = this; let letters = document.getElementById('available_letters'); clearElement(letters); let space = document.createElement('button'); space.innerText = '\xA0'; //   space.addEventListener('click', button => ui.clearCurrent()); letters.appendChild(space); shuffle(this.game.availableLetters).forEach(function(letter) { let button = document.createElement('button'); button.innerText = letter; button.addEventListener('click', button => ui.enterLetter(letter)); letters.appendChild(button); }); this.recolorAvailableLetters(); } setFocusedTextbox(textbox) { this.setFocusedTextboxIndex(this.textboxes.indexOf(textbox)); } setFocusedTextboxIndex(i) { if (i < 0) i = 0; this.focusedTextbox = i; while (this.focusedTextbox < this.game.numLetters && this.locks[this.focusedTextbox].checked) { this.focusedTextbox++; } if (this.focusedTextbox >= this.game.numLetters) { this.focusedTextbox = this.game.numLetters - 1; while (this.focusedTextbox > 0 && this.locks[this.focusedTextbox].checked) { this.focusedTextbox--; } if (this.focusedTextbox == 0 && this.locks[0].checked && i != 0) { this.focusedTextbox = i; } } for (let j = 0; j < this.textboxes.length; j++) { this.textboxes[j].parentNode .classList.toggle('focused', j == this.focusedTextbox); } this.recolorAvailableLetters(); } focusPreviousTextbox() { let i = this.focusedTextbox - 1; while (i > 0 && this.locks[i].checked) { i--; } this.setFocusedTextboxIndex(i); } focusNextTextbox() { let i = this.focusedTextbox + 1; if (i < this.game.numLetters) { this.setFocusedTextboxIndex(i); } } mayEnterLetter(letter) { return this.game.availableLetters.includes(letter); } enterLetter(letter) { if (!this.mayEnterLetter(letter)) { this.textboxes[this.focusedTextbox].innerText = this.letters[this.focusedTextbox] || '\xA0'; return; } if (this.locks[this.focusedTextbox].checked) { this.focusNextTextbox(); return; } this.textboxes[this.focusedTextbox].innerText = letter; this.letters[this.focusedTextbox] = letter; this.setFocusedTextboxIndex(this.focusedTextbox + 1); if (this.autoSubmit.checked) this.submitWord(); } textEntered(textbox) { let index = this.textboxes.indexOf(textbox); if (this.locks[index].checked) { textbox.innerText = this.letters[index]; } else { this.setFocusedTextboxIndex(index); this.enterLetter(textbox.innerText.slice(-1).toLowerCase()); } } clearAll() { for (let i = 0; i < this.game.numLetters; i++) { this.locks[i].checked = false; } this.clearUnlocked(); } clearUnlocked() { for (let i = 0; i < this.game.numLetters; i++) { if (!this.locks[i].checked) { this.textboxes[i].innerText = '\xA0'; this.letters[i] = ''; } } this.setFocusedTextboxIndex(0); } clearCurrent() { let i = this.focusedTextbox; if (this.locks[i].checked) return; this.textboxes[i].innerText = '\xA0'; this.letters[i] = ''; } setSettingsForm(obj) { let els = document.forms['difficulty'].elements; for (let key in obj) { if (key === 'auto_submit') continue; els[key].value = obj[key]; } } getSettingsObjectFromForm() { let els = document.forms['difficulty'].elements; let res = {}; let default_settings = this.defaultSettings; for (let key in default_settings) { if (!els[key].checkValidity()) return null; res[key] = Number(els[key].value); } if (res.min_word_length > res.max_word_length || res.min_matches > res.max_matches) { return null; } return res; } updateSavedGamesMenu(settings) { let saves = settings['saved_games']; let gameSelect = document.getElementById('available_games'); let gameFragment = this.game ? this.game.fragment : null; clearElement(gameSelect); for (let fragment in saves) { let option = document.createElement('option'); option.value = fragment; option.selected = fragment == gameFragment; option.innerText = (option.selected ? '[*] ' : '') + new Date(saves[fragment].last_played) + " - [" + saves[fragment].guesses.length + "] " + (saves[fragment].guesses.slice(-1)[0] || "(no guesses yet)"); gameSelect.appendChild(option); } } newGame() { let settings = this.getSettingsObjectFromForm(); if (settings === null) return; let game; do { game = this.gameClass.generateRandom(settings); } while (game === null && window.confirm('Unable to generate puzzle with the given difficulty settings. Try again?')); if (game !== null) this.initialize(game); } doPostInitialize() { let fragment = this.game.fragment; let currentSettings = this.loadSettingsJson(); if (!currentSettings) currentSettings = this.defaultSettings; if (!('saved_games' in currentSettings)) { currentSettings['saved_games'] = {}; } if (fragment in currentSettings.saved_games) { currentSettings.saved_games[fragment].guesses.forEach(guess => { this.submitWord(guess); }); currentSettings.saved_games[fragment].last_played = new Date().toJSON(); } else { currentSettings.saved_games[fragment] = { 'guesses': [], 'last_played': new Date().toJSON() }; } this.saveSettingsJson(currentSettings); this.updateSavedGamesMenu(currentSettings); } getRecentGameFragment() { let currentSettings = this.loadSettingsJson(); if (!currentSettings) return null; if (!('saved_games' in currentSettings)) return null; let saves = currentSettings.saved_games; if (!saves) return null; let recentDate = Object.keys(saves) .map(fragment => saves[fragment].last_played) .sort() .slice(-1)[0]; for (let fragment in saves) { if (saves[fragment].last_played == recentDate) { return fragment; } } return null; } nextGame(game) { if (this.won) { this.finishGame(); } if (!game) { let fragment = this.getRecentGameFragment(); game = this.gameClass.fromFragment(fragment); } if (game === null) { this.newGame(); } else { this.initialize(game); } } finishGame() { if (this.won) { let fragment = this.game.fragment; let currentSettings = this.loadSettingsJson(); delete currentSettings.saved_games[fragment]; this.saveSettingsJson(currentSettings); this.updateSavedGamesMenu(currentSettings); } } saveGuess(guess) { let fragment = this.game.fragment; let currentSettings = this.loadSettingsJson(); currentSettings.saved_games[fragment].guesses.push(guess); currentSettings.saved_games[fragment].last_played = new Date().toJSON(); this.saveSettingsJson(currentSettings); this.updateSavedGamesMenu(currentSettings); } loadSettingsJson() { let json = window.localStorage.getItem(this.settingsKey); if (json) { try { return JSON.parse(json); } catch (e) {} } return null; } loadSettings() { let default_settings = this.defaultSettings; let obj = this.loadSettingsJson(); if (obj) { let settings = {}; for (let key in default_settings) { if (key in obj) { settings[key] = obj[key]; } else { settings[key] = default_settings[key]; } } if ('auto_submit' in obj) { document.getElementById('auto_submit').checked = obj.auto_submit; } if ('saved_games' in obj) { this.updateSavedGamesMenu(obj); } this.setSettingsForm(settings); } else { this.setSettingsForm(default_settings); } } saveSettings() { let settings = this.getSettingsObjectFromForm(); if (settings === null) return; settings['auto_submit'] = this.autoSubmit.checked; let currentSettings = this.loadSettingsJson(); if (currentSettings) { settings['saved_games'] = currentSettings['saved_games']; } this.saveSettingsJson(settings); } saveSettingsJson(settings) { let json = JSON.stringify(settings); window.localStorage.setItem(this.settingsKey, json); } }