457 lines
16 KiB
JavaScript
457 lines
16 KiB
JavaScript
'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.autoSubmit = document.getElementById('auto_submit');
|
|
this.submit = document.getElementById('submit');
|
|
|
|
this.loadSettings();
|
|
let ui = this;
|
|
|
|
this.submit
|
|
.addEventListener('click', _ => ui.submitWord());
|
|
this.autoSubmit.addEventListener('change', _ => {
|
|
ui.saveSettings();
|
|
ui.submit.style.display = ui.autoSubmit.checked ? 'none' : '';
|
|
});
|
|
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();
|
|
});
|
|
|
|
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) {
|
|
this.autoSubmit.checked = obj.auto_submit;
|
|
|
|
this.submit.style.display = this.autoSubmit.checked ? 'none' : '';
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|