From 4f821372faecef53c3a903789e1129b927d01479 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sun, 24 Apr 2016 17:26:39 +0200 Subject: [PATCH 01/14] Add settings form and update affected CSS I added two dummy checkboxes to give an idea of what the settings may look like. --- ZATACKA.html | 10 ++++++++++ Zatacka.css | 25 +++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/ZATACKA.html b/ZATACKA.html index 2d6a576..5148e60 100644 --- a/ZATACKA.html +++ b/ZATACKA.html @@ -47,6 +47,16 @@
+
+

+ + +

+

+ + +

+
+ diff --git a/Zatacka.css b/Zatacka.css index cf93867..a608a74 100644 --- a/Zatacka.css +++ b/Zatacka.css @@ -59,6 +59,24 @@ input[type="checkbox"]:checked + label::before { background-position: center center; } +.icon-button { + background-image: url("resources/kurve-icons.png"); + cursor: pointer; + height: 16px; + opacity: 0.75; + width: 16px; +} + +.icon-button:hover, .icon-button:focus, .icon-button:active { + opacity: 1; +} + +.corner { + position: absolute; + right: 16px; + top: 16px; +} + #debug { background-color: black; border: 1px white solid; @@ -122,6 +140,10 @@ input[type="checkbox"]:checked + label::before { box-sizing: border-box; } +.hidden { + display: none; +} + .unobtrusive { pointer-events: none; } @@ -131,10 +153,6 @@ input[type="checkbox"]:checked + label::before { overflow: hidden; } -#lobby.hidden, #KONEC_HRY.hidden { - display: none; -} - #lobby { padding-left: 81px; padding-top: 50px; @@ -184,13 +202,20 @@ input[type="checkbox"]:checked + label::before { #lobby #controls .blue.controls { background-image: url("resources/kurve-lobby-controls-blue.png"); } #lobby #controls .blue.ready { background-image: url("resources/kurve-lobby-ready-blue.png"); } -#lobby #settings { - display: inline-block; - padding-left: 8px; - vertical-align: top; +#button-show-settings { + background-position: 0 -16px; +} + +#button-hide-settings { + background-position: 0 0; +} + +#settings { + padding: 50px 80px 50px 80px; + background-color: rgba(0, 0, 0, 0.9); } -#lobby #settings > p { +#settings > p { margin-bottom: 4px; } diff --git a/js/Zatacka.js b/js/Zatacka.js index 8239bfd..edd40b0 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -4,6 +4,7 @@ const Zatacka = ((window, document) => { const canvas_main = byID("canvas_main"); const canvas_overlay = byID("canvas_overlay"); + const element_settings = byID("settings"); const ORIGINAL_WIDTH = canvas_main.width; const ORIGINAL_HEIGHT = canvas_main.height; const TOTAL_BORDER_THICKNESS = 4; @@ -191,6 +192,10 @@ const Zatacka = ((window, document) => { } } + function eventConsumer(event) { + event.stopPropagation(); + } + function keyPressedInLobby(pressedKey) { config.defaultPlayers.forEach((playerData) => { addOrRemovePlayer(playerData, pressedKey); @@ -236,8 +241,55 @@ const Zatacka = ((window, document) => { event.preventDefault(); } + function settingsKeyHandler(event) { + const pressedKey = event.keyCode; + if (isQuitKey(pressedKey)) { + hideSettings(); + } + } + + function showSettings() { + clearTimeout(hintPickTimer); + clearTimeout(hintProceedTimer); + removeLobbyEventListeners(); + addHideSettingsButtonEventListener(); + document.addEventListener("keydown", settingsKeyHandler); + element_settings.classList.remove("hidden"); + } + + function hideSettings() { + document.removeEventListener("keydown", settingsKeyHandler); + addLobbyEventListeners(); + element_settings.classList.add("hidden"); + } + + function addShowSettingsButtonEventListener() { + const showSettingsButton = byID("button-show-settings"); + if (showSettingsButton instanceof HTMLElement) { + showSettingsButton.addEventListener("mousedown", eventConsumer); + showSettingsButton.addEventListener("click", showSettings); + } + } + + function addHideSettingsButtonEventListener() { + const hideSettingsButton = byID("button-hide-settings"); + if (hideSettingsButton instanceof HTMLElement) { + hideSettingsButton.addEventListener("mousedown", eventConsumer); + hideSettingsButton.addEventListener("click", hideSettings); + } + } + + function removeShowSettingsButtonEventListener() { + const showSettingsButton = byID("button-show-settings"); + if (showSettingsButton instanceof HTMLElement) { + showSettingsButton.removeEventListener("mousedown", eventConsumer); + showSettingsButton.removeEventListener("click", showSettings); + } + } + function addLobbyEventListeners() { log("Adding lobby event listeners ..."); + addShowSettingsButtonEventListener(); document.addEventListener("keydown", lobbyKeyHandler); document.addEventListener("mousedown", lobbyMouseHandler); document.addEventListener("contextmenu", lobbyMouseHandler); @@ -246,6 +298,7 @@ const Zatacka = ((window, document) => { function removeLobbyEventListeners() { log("Removing lobby event listeners ..."); + removeShowSettingsButtonEventListener(); document.removeEventListener("keydown", lobbyKeyHandler); document.removeEventListener("mousedown", lobbyMouseHandler); document.removeEventListener("contextmenu", lobbyMouseHandler); diff --git a/resources/kurve-icons.png b/resources/kurve-icons.png new file mode 100644 index 0000000000000000000000000000000000000000..b9bd2c4bc3edb6b758baab952d2a2f09fbc85e90 GIT binary patch literal 1105 zcmV-X1g`suP)U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<8AY({UO#lFTB>(_`g8%^e z{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ+vq?ljRCwC#lEDtcFbG3!)Zgvb?1SAy zn3{a`-`0S_PBzivPOk@C> zv@nt7!8~GlFpu;_(P_Ig{F$3eKyCvew#f}>U$h|TUEAh&^sWM(_l)t8&Rv{7fsA>GU-0Bs8!v~F=}`{?GM4yJ$t3Mim}0tzUgfC36A05}E! XgtstZ)MZ6800000NkvXXu0mjfZe`hA literal 0 HcmV?d00001 From 5d97e604b7f51007648471e9c871259e111e236e Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sat, 11 Jun 2016 11:48:16 +0200 Subject: [PATCH 03/14] Fix CSS issue that made #lobby always visible With `#lobby { display: flex; }` having higher specificity than `.hidden { display: none; }`, the lobby would never be hidden when starting a game. --- ZATACKA.html | 2 +- Zatacka.css | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ZATACKA.html b/ZATACKA.html index ef6f19d..0f00fa0 100644 --- a/ZATACKA.html +++ b/ZATACKA.html @@ -63,7 +63,7 @@

Achtung, die Kurve!

-
+
  • diff --git a/Zatacka.css b/Zatacka.css index 5dfacb3..dc17d90 100644 --- a/Zatacka.css +++ b/Zatacka.css @@ -194,6 +194,10 @@ input[type="checkbox"]:checked + label::before { border-color: #515151; } +.flex { + display: flex; +} + .overlay { display: block; height: 100%; @@ -221,7 +225,6 @@ input[type="checkbox"]:checked + label::before { padding-left: 81px; padding-top: 50px; padding-bottom: 130px; /* 130 = 480 - 7 * 50, where 50 is the total height of each player's controls and 7 is 6 players plus 1 for margin */ - display: flex; /* to prevent HTML whitespace from being displayed */ } #lobby #controls { From fca455b4dd70a9319139390f623dd9d554ba41fb Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sat, 16 Jul 2016 23:01:15 +0200 Subject: [PATCH 04/14] Add basic settings GUI --- ZATACKA.html | 14 +---- Zatacka.css | 24 ++++++- js/GUIController.js | 70 +++++++++++++++++++++ js/Zatacka.js | 18 +++++- js/lib/preferences/BooleanPreference.js | 1 + js/lib/preferences/MultichoicePreference.js | 1 + js/lib/preferences/Preference.js | 1 + js/lib/preferences/PreferenceManager.js | 5 ++ js/lib/preferences/PreferenceWithValue.js | 11 ++++ js/locales/Zatacka.en_US.properties | 10 +++ js/strings.js | 1 + 11 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 js/lib/preferences/PreferenceWithValue.js diff --git a/ZATACKA.html b/ZATACKA.html index 0f00fa0..d7027de 100644 --- a/ZATACKA.html +++ b/ZATACKA.html @@ -104,17 +104,8 @@

    Achtung, die Kurve!

@@ -149,6 +140,7 @@

Achtung, die Kurve!

+ diff --git a/Zatacka.css b/Zatacka.css index dc17d90..3c38332 100644 --- a/Zatacka.css +++ b/Zatacka.css @@ -279,11 +279,29 @@ input[type="checkbox"]:checked + label::before { #settings { padding: 50px 80px 50px 80px; - background-color: rgba(0, 0, 0, 0.9); + background-color: rgba(0, 0, 0, 0.95); } -#settings > p { - margin-bottom: 4px; +#settings-form p { + min-height: 25px; +} + +#settings-form label { + display: inline-block; + text-align: right; + width: 190px; +} + +#settings-form label::after { + content: ":"; +} + +#settings-form select { + background: black; + border: 1px solid white; + color: white; + margin-left: 20px; + width: 189px; } #messages { diff --git a/js/GUIController.js b/js/GUIController.js index 6a904fb..9995c79 100644 --- a/js/GUIController.js +++ b/js/GUIController.js @@ -16,6 +16,8 @@ function GUIController(cfg) { const results = byID("results"); const KONEC_HRY = byID("KONEC_HRY"); const messagesContainer = byID("messages"); + const settingsContainer = byID("settings"); + const settingsForm = byID("settings-form"); const ORIGINAL_LEFT_WIDTH = left.offsetWidth; @@ -71,6 +73,56 @@ function GUIController(cfg) { setCursorBehavior(CURSOR_VISIBLE); } + function settingsEntryHTMLElement(preference, preferenceValue) { + if (!preference instanceof Preference) { + throw new TypeError(`${preference} is not a preference.`); + } + + // Common + const p = document.createElement("p"); + const label = document.createElement("label"); + label.textContent = preference.label; + label.for = `${STRINGS.html_name_preference_prefix}${preference.key}`; + + // Boolean + if (preference instanceof BooleanPreference) { + const input = document.createElement("input"); + input.type = "checkbox"; + input.name = STRINGS.html_name_preference_prefix + preference.key; + input.checked = preferenceValue === true; + p.appendChild(input); + p.appendChild(label); + } + + // Multichoice + else if (preference instanceof MultichoicePreference) { + p.appendChild(label); + const select = document.createElement("select"); + select.name = STRINGS.html_name_preference_prefix + preference.key; + preference.values.forEach((value, index) => { + const option = document.createElement("option"); + option.value = value; + option.textContent = preference.labels[index]; + if (preference.constructor.stringify(preferenceValue) === value) { + option.selected = true; + } + select.appendChild(option); + }); + p.appendChild(select); + } + + // Range + else if (preference instanceof RangePreference) { + p.appendChild(label); + const input = document.createElement("input"); + input.type = "number"; + input.name = STRINGS.html_name_preference_prefix + preference.key; + p.appendChild(input); + } + + return p; + } + // PUBLIC API @@ -138,6 +190,21 @@ function GUIController(cfg) { updateMessages(currentMessages); } + function showSettings() { + settings.classList.remove(STRINGS.class_hidden); + } + + function hideSettings() { + settings.classList.add(STRINGS.class_hidden); + } + + function updateSettingsForm(preferencesWithData) { + flush(settingsForm); + preferencesWithData.forEach((preferenceWithData) => { + settingsForm.appendChild(settingsEntryHTMLElement(preferenceWithData.preference, preferenceWithData.value)); + }); + } + function hideMessage(message) { currentMessages = currentMessages.filter(msg => msg !== message); updateMessages(currentMessages); @@ -198,6 +265,9 @@ function GUIController(cfg) { gameStarted, gameQuit, konecHry, + showSettings, + hideSettings, + updateSettingsForm, updateScoreOfPlayer, updateMessages, showMessage, diff --git a/js/Zatacka.js b/js/Zatacka.js index fe43f16..ef571ab 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -4,7 +4,6 @@ const Zatacka = ((window, document) => { const canvas_main = byID("canvas_main"); const canvas_overlay = byID("canvas_overlay"); - const element_settings = byID("settings"); const ORIGINAL_WIDTH = canvas_main.width; const ORIGINAL_HEIGHT = canvas_main.height; const TOTAL_BORDER_THICKNESS = 4; @@ -53,21 +52,33 @@ const Zatacka = ((window, document) => { { type: MultichoicePreference, key: STRINGS.pref_key_cursor, + label: TEXT.pref_label_cursor, values: [ STRINGS.pref_value_cursor_always_visible, STRINGS.pref_value_cursor_hidden_when_mouse_used_by_player, STRINGS.pref_value_cursor_always_hidden ], + labels: [ + TEXT.pref_label_cursor_always_visible, + TEXT.pref_label_cursor_hidden_when_mouse_used_by_player, + TEXT.pref_label_cursor_always_hidden + ], default: STRINGS.pref_value_cursor_hidden_when_mouse_used_by_player }, { type: MultichoicePreference, key: STRINGS.pref_key_hints, + label: TEXT.pref_label_hints, values: [ STRINGS.pref_value_hints_all, STRINGS.pref_value_hints_warnings_only, STRINGS.pref_value_hints_none ], + labels: [ + TEXT.pref_label_hints_all, + TEXT.pref_label_hints_warnings_only, + TEXT.pref_label_hints_none + ], default: STRINGS.pref_value_hints_all } ]); @@ -314,16 +325,17 @@ const Zatacka = ((window, document) => { function showSettings() { clearTimeout(hintPickTimer); clearTimeout(hintProceedTimer); + guiController.updateSettingsForm(preferenceManager.getAllPreferencesWithValues()); removeLobbyEventListeners(); addHideSettingsButtonEventListener(); document.addEventListener("keydown", settingsKeyHandler); - element_settings.classList.remove("hidden"); + guiController.showSettings(); } function hideSettings() { document.removeEventListener("keydown", settingsKeyHandler); addLobbyEventListeners(); - element_settings.classList.add("hidden"); + guiController.hideSettings(); } function addShowSettingsButtonEventListener() { diff --git a/js/lib/preferences/BooleanPreference.js b/js/lib/preferences/BooleanPreference.js index 93d3b45..353cdcc 100644 --- a/js/lib/preferences/BooleanPreference.js +++ b/js/lib/preferences/BooleanPreference.js @@ -5,6 +5,7 @@ class BooleanPreference extends MultichoicePreference { super({ key: data.key, values: ["true", "false"], + label: data.label, default: data.default }); } diff --git a/js/lib/preferences/MultichoicePreference.js b/js/lib/preferences/MultichoicePreference.js index 73ca245..6c654d0 100644 --- a/js/lib/preferences/MultichoicePreference.js +++ b/js/lib/preferences/MultichoicePreference.js @@ -7,6 +7,7 @@ class MultichoicePreference extends Preference { } super(data); this.values = data.values; + this.labels = data.labels; if (!this.isValidValue(data.default)) { super.invalidValue(data.default); } diff --git a/js/lib/preferences/Preference.js b/js/lib/preferences/Preference.js index 5ee563e..166e067 100644 --- a/js/lib/preferences/Preference.js +++ b/js/lib/preferences/Preference.js @@ -8,6 +8,7 @@ class Preference { throw new TypeError(`Preference '${data.key}' must have a default value.`); } this.key = data.key; + this.label = data.label; this.default = data.default; } diff --git a/js/lib/preferences/PreferenceManager.js b/js/lib/preferences/PreferenceManager.js index 79dc539..9bed277 100644 --- a/js/lib/preferences/PreferenceManager.js +++ b/js/lib/preferences/PreferenceManager.js @@ -31,6 +31,10 @@ function PreferenceManager(preferencesData) { return PREFERENCES.find((pref) => pref.key === key); } + function getAllPreferencesWithValues() { + return PREFERENCES.map((preference) => new PreferenceWithValue(preference, get(preference.key))); + } + function getKey(pref) { return pref.key; } @@ -88,6 +92,7 @@ function PreferenceManager(preferencesData) { get, setToDefaultValue, getDefaultValue, + getAllPreferencesWithValues, setAllToDefault } } \ No newline at end of file diff --git a/js/lib/preferences/PreferenceWithValue.js b/js/lib/preferences/PreferenceWithValue.js new file mode 100644 index 0000000..ee00955 --- /dev/null +++ b/js/lib/preferences/PreferenceWithValue.js @@ -0,0 +1,11 @@ +"use strict"; + +class PreferenceWithValue { + constructor(preference, value) { + if (!preference.isValidValue(value)) { + throw new TypeError(`${value} is not a valid value for preference ${preference.key}.`); + } + this.preference = preference; + this.value = value; + } +} diff --git a/js/locales/Zatacka.en_US.properties b/js/locales/Zatacka.en_US.properties index 1987a99..d1c8b44 100644 --- a/js/locales/Zatacka.en_US.properties +++ b/js/locales/Zatacka.en_US.properties @@ -8,4 +8,14 @@ const TEXT = Object.freeze({ hint_alt: `Alt plus some other keys (e.g. Tab) may cause undesired behavior (e.g. switching windows).`, hint_ctrl: `Ctrl plus some other keys (e.g. W) may cause undesired behavior (e.g. closing the tab).`, hint_mouse: `Make sure to keep the mouse cursor inside the browser window.`, + + pref_label_cursor: `Cursor`, + pref_label_cursor_always_visible: "Always visible", + pref_label_cursor_hidden_when_mouse_used_by_player: `Hidden when mouse used`, + pref_label_cursor_always_hidden: "Always hidden", + + pref_label_hints: `Hints`, + pref_label_hints_all: "All", + pref_label_hints_warnings_only: "Warnings only", + pref_label_hints_none: "None", }); diff --git a/js/strings.js b/js/strings.js index 6f485ec..0da4fac 100644 --- a/js/strings.js +++ b/js/strings.js @@ -6,6 +6,7 @@ const STRINGS = Object.freeze({ class_hidden: "hidden", class_active: "active", class_nocursor: "nocursor", + html_name_preference_prefix: "preference-", pref_key_cursor: "cursor", pref_value_cursor_always_visible: "always_visible", From 7b6d9ebd68a0441340bc81c76bf1c9b5dad3cdec Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Mon, 18 Jul 2016 14:03:52 +0200 Subject: [PATCH 05/14] Implement settings functionality and improve GUI As of this commit, the settings form works: Changes are saved and applied when closing it and saved between page loads. Multichoice preferences are now represented as radio buttons, since select boxes do not work with CSS transform in Firefox. Settings now have descriptions which are shown when hovering over them in the settings menu. These are part of the preferences backend. The "Prevent spawnkills" settings is a dummy and does not work yet. --- Zatacka.css | 115 +++++++++++++++----- js/GUIController.js | 131 ++++++++++++++++------- js/Zatacka.js | 70 +++++++++--- js/lib/preferences/BooleanPreference.js | 16 +-- js/lib/preferences/Preference.js | 1 + js/lib/preferences/PreferenceManager.js | 4 +- js/locales/Zatacka.en_US.properties | 25 +++-- js/strings.js | 13 +++ resources/kurve-radio-button-checked.png | Bin 0 -> 2850 bytes resources/kurve-radio-button.png | Bin 0 -> 2845 bytes 10 files changed, 285 insertions(+), 90 deletions(-) create mode 100644 resources/kurve-radio-button-checked.png create mode 100644 resources/kurve-radio-button.png diff --git a/Zatacka.css b/Zatacka.css index 3c38332..b1b3824 100644 --- a/Zatacka.css +++ b/Zatacka.css @@ -25,43 +25,61 @@ h1 { display: none; } -input[type="checkbox"] { +input[type="checkbox"], +input[type="radio"] { display: none; } /* Checkbox and label: */ -input[type="checkbox"] + label { - cursor: pointer; - opacity: 0.75; +input[type="checkbox"] + label, +input[type="radio"] + label { padding-left: 18px; position: relative; } -/* Checkbox and label on hover: */ -input[type="checkbox"] + label:hover, -input[type="checkbox"] + label:focus, -input[type="checkbox"] + label:active { - opacity: 1; -} - /* Actual checkbox: */ -input[type="checkbox"] + label::before { - border: 1px white solid; +input[type="checkbox"] + label::before, +input[type="radio"] + label::before { + background-position: center center; + background-repeat: no-repeat; box-sizing: border-box; content: ""; display: inline-block; height: 12px; left: 0; position: absolute; - top: 1px; + top: 2px; width: 12px; } +input[type="checkbox"] + label::before { + border: 1px white solid; +} + +input[type="radio"] + label::before { + background-image: url("resources/kurve-radio-button.png"); +} + /* Actual checkbox when checked: */ input[type="checkbox"]:checked + label::before { background-image: url("resources/kurve-checkbox-tickmark.png"); - background-position: center center; - background-repeat: no-repeat; +} + +input[type="radio"]:checked + label::before { + background-image: url("resources/kurve-radio-button-checked.png"); +} + +input[type="checkbox"] + label, +input[type="radio"] + label { + padding-top: 1px; + padding-bottom: 1px; +} + +/* Checkbox and label on hover: */ +input[type="checkbox"] + label, +input[type="radio"] + label, +select { + cursor: pointer; } .button { @@ -278,30 +296,68 @@ input[type="checkbox"]:checked + label::before { } #settings { - padding: 50px 80px 50px 80px; background-color: rgba(0, 0, 0, 0.95); + padding: 50px 80px 50px 80px; } -#settings-form p { - min-height: 25px; +#settings-form > div { + box-sizing: border-box; + margin: 12px 0; + opacity: 0.6; +} + +#settings-form > div:hover { + opacity: 1; +} + +#settings-form > div:hover .description { + visibility: visible; +} + +#settings-form fieldset { + border: 1px solid white; + padding: 2px 8px 6px 8px; +} + +#settings-form fieldset legend { + padding: 0 4px; +} + +#settings-form input[type="checkbox"] + label { + margin-left: 13px; /* to align with fieldset content */ +} + +#settings-form fieldset input[type="radio"] + label { + margin-left: 4px; /* to align with legend */ } #settings-form label { display: inline-block; - text-align: right; - width: 190px; } -#settings-form label::after { - content: ":"; +#settings-form .description { + box-sizing: border-box; + bottom: 0; + font-size: 0.75em; + height: 50px; + left: 0; + opacity: 0.8; + padding: 0 80px; + position: absolute; + visibility: hidden; + width: 100%; } #settings-form select { background: black; border: 1px solid white; color: white; - margin-left: 20px; - width: 189px; + margin-top: 2px; + width: 219px; +} + +#settings-form input[type="radio"] + label { + display: block; } #messages { @@ -323,6 +379,15 @@ input[type="checkbox"]:checked + label::before { color: #C3C300; } +#messages.hints-none .message, +#messages.hints-warnings-only .message { + display: none; +} + +#messages.hints-warnings-only .message.warning { + display: block; +} + .canvasHeight { height: 480px; } diff --git a/js/GUIController.js b/js/GUIController.js index 9995c79..ad7b74d 100644 --- a/js/GUIController.js +++ b/js/GUIController.js @@ -2,10 +2,6 @@ function GUIController(cfg) { - const CURSOR_VISIBLE = "visible"; - const CURSOR_HIDDEN_ON_CANVAS = "hidden_on_canvas"; - const CURSOR_HIDDEN = "hidden"; - const config = cfg; const lobby = byID("lobby"); const controls = byID("controls"); @@ -54,14 +50,10 @@ function GUIController(cfg) { function setCursorBehavior(behavior) { switch (behavior) { - case CURSOR_VISIBLE: + case STRINGS.cursor_visible: document.body.classList.remove(STRINGS.class_nocursor); break; - case CURSOR_HIDDEN_ON_CANVAS: - canvas_main.classList.add(STRINGS.class_nocursor); - canvas_overlay.classList.add(STRINGS.class_nocursor); - break; - case CURSOR_HIDDEN: + case STRINGS.cursor_hidden: document.body.classList.add(STRINGS.class_nocursor); break; default: @@ -69,58 +61,81 @@ function GUIController(cfg) { } } - function resetCursorBehavior() { - setCursorBehavior(CURSOR_VISIBLE); - } - function settingsEntryHTMLElement(preference, preferenceValue) { if (!preference instanceof Preference) { throw new TypeError(`${preference} is not a preference.`); } // Common - const p = document.createElement("p"); + const div = document.createElement("div"); const label = document.createElement("label"); label.textContent = preference.label; - label.for = `${STRINGS.html_name_preference_prefix}${preference.key}`; + label.setAttribute("for", `${STRINGS.html_name_preference_prefix}${preference.key}`); + const description = document.createElement("aside"); + description.textContent = preference.description; + description.classList.add(STRINGS.class_description); // Boolean if (preference instanceof BooleanPreference) { const input = document.createElement("input"); input.type = "checkbox"; - input.name = STRINGS.html_name_preference_prefix + preference.key; + input.dataset.key = preference.key; + input.id = STRINGS.html_name_preference_prefix + preference.key; input.checked = preferenceValue === true; - p.appendChild(input); - p.appendChild(label); + div.appendChild(input); + div.appendChild(label); } // Multichoice else if (preference instanceof MultichoicePreference) { - p.appendChild(label); - const select = document.createElement("select"); - select.name = STRINGS.html_name_preference_prefix + preference.key; + // div.appendChild(label); + // const select = document.createElement("select"); + // select.id = STRINGS.html_name_preference_prefix + preference.key; + // select.dataset.key = preference.key; + // preference.values.forEach((value, index) => { + // const option = document.createElement("option"); + // option.value = value; + // option.textContent = preference.labels[index]; + // if (preference.constructor.stringify(preferenceValue) === value) { + // option.selected = true; + // } + // select.appendChild(option); + // }); + // div.appendChild(select); + + const fieldset = document.createElement("fieldset"); + const legend = document.createElement("legend"); + legend.textContent = preference.label; + fieldset.appendChild(legend); preference.values.forEach((value, index) => { - const option = document.createElement("option"); - option.value = value; - option.textContent = preference.labels[index]; - if (preference.constructor.stringify(preferenceValue) === value) { - option.selected = true; - } - select.appendChild(option); + const id = STRINGS.html_name_preference_prefix + preference.key + "-" + preference.values[index]; + const radioButton = document.createElement("input"); + radioButton.type = "radio"; + radioButton.id = id; + radioButton.name = STRINGS.html_name_preference_prefix + preference.key; + radioButton.value = value; + radioButton.dataset.key = preference.key; + radioButton.checked = preferenceValue === value; + const radioButtonLabel = document.createElement("label"); + radioButtonLabel.textContent = preference.labels[index]; + radioButtonLabel.setAttribute("for", id); + fieldset.appendChild(radioButton); + fieldset.appendChild(radioButtonLabel); }); - p.appendChild(select); + div.appendChild(fieldset); } // Range else if (preference instanceof RangePreference) { - p.appendChild(label); + div.appendChild(label); const input = document.createElement("input"); input.type = "number"; input.name = STRINGS.html_name_preference_prefix + preference.key; - p.appendChild(input); + div.appendChild(input); } - return p; + div.appendChild(description); + return div; } @@ -167,7 +182,7 @@ function GUIController(cfg) { resetScoreboard(); resetResults(); allPlayersUnready(); - resetCursorBehavior(); + setCursorBehavior(STRINGS.cursor_visible); } function konecHry() { @@ -205,6 +220,32 @@ function GUIController(cfg) { }); } + function parseSettingsForm() { + const newSettings = []; + // elements: + const inputs = settingsForm.querySelectorAll("input"); + inputs.forEach((input) => { + if (input.type === "checkbox") { + // checkbox + newSettings.push({ key: input.dataset.key, value: input.checked }); + } else if (input.type === "radio") { + // radio + if (input.checked === true) { + newSettings.push({ key: input.dataset.key, value: input.value }); + } + } else { + // text, number etc + newSettings.push({ key: input.dataset.key, value: input.value.toString() }); + } + }); + // in settings GUI --- js/GUIController.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/js/GUIController.js b/js/GUIController.js index ad7b74d..50c9b43 100644 --- a/js/GUIController.js +++ b/js/GUIController.js @@ -88,21 +88,6 @@ function GUIController(cfg) { // Multichoice else if (preference instanceof MultichoicePreference) { - // div.appendChild(label); - // const select = document.createElement("select"); - // select.id = STRINGS.html_name_preference_prefix + preference.key; - // select.dataset.key = preference.key; - // preference.values.forEach((value, index) => { - // const option = document.createElement("option"); - // option.value = value; - // option.textContent = preference.labels[index]; - // if (preference.constructor.stringify(preferenceValue) === value) { - // option.selected = true; - // } - // select.appendChild(option); - // }); - // div.appendChild(select); - const fieldset = document.createElement("fieldset"); const legend = document.createElement("legend"); legend.textContent = preference.label; From 211da4bd7136da251ab176d2577ff8ac7e69f428 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sat, 6 Aug 2016 01:53:21 +0200 Subject: [PATCH 08/14] Add simple unload blocker Just the standard dialog on beforeunload. Triggering it may sometimes cause one or more Kurves to not draw anything for ~20px. No obvious pattern can be seen. The only reason for this behavior that I can conceive is MainLoop panicking, but I am not sure how one could solve it. Anyway, I think it is better than accidentally unloading the entire game due to a misclick. --- js/Zatacka.js | 9 +++++++++ js/locales/Zatacka.en_US.js | 1 + 2 files changed, 10 insertions(+) diff --git a/js/Zatacka.js b/js/Zatacka.js index e5aadf7..44a6340 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -339,6 +339,13 @@ const Zatacka = ((window, document) => { event.preventDefault(); } + function gameUnloadHandler(event) { + // A simple trick to prevent accidental unloading of the entire game. + const message = TEXT.hint_unload; + event.returnValue = message; // Gecko, Trident, Chrome 34+ + return TEXT.hint_unload; // Gecko, Webkit, Chrome <34 + } + function settingsKeyHandler(event) { const pressedKey = event.keyCode; if (isQuitKey(pressedKey)) { @@ -433,6 +440,7 @@ const Zatacka = ((window, document) => { document.addEventListener("keydown", gameKeyHandler); document.addEventListener("mousedown", gameMouseHandler); document.addEventListener("contextmenu", gameMouseHandler); + window.addEventListener("beforeunload", gameUnloadHandler); log("Done."); } @@ -440,6 +448,7 @@ const Zatacka = ((window, document) => { log("Removing game event listeners ..."); document.removeEventListener("keydown", gameKeyHandler); document.removeEventListener("mousedown", gameMouseHandler); + window.removeEventListener("beforeunload", gameUnloadHandler); log("Done."); } diff --git a/js/locales/Zatacka.en_US.js b/js/locales/Zatacka.en_US.js index c11fa2e..aad85fc 100644 --- a/js/locales/Zatacka.en_US.js +++ b/js/locales/Zatacka.en_US.js @@ -5,6 +5,7 @@ const TEXT = (() => { const KEY_CMD = "⌘"; return Object.freeze({ + hint_unload: `Are you sure you want to unload the page?`, hint_start: `Press Space to start`, hint_pick: `Pick your desired color by pressing the corresponding LEFT key (e.g. M for Orange).`, hint_proceed: `Press Space or Enter to start!`, From e7602ae7d529842985876024f7c8a70e382b7192 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sat, 11 Mar 2017 21:08:33 +0100 Subject: [PATCH 09/14] Add error handling for localStorage access denied Before this commit, if the browser was set not to allow access to localStorage (e.g. "Block third-party cookies and site data" in Chrome), things would crash, the settings menu would not show etc. This commit adds error handling for this and displays a message telling the user that settings could not be loaded/saved. Settings can also be changed and used as expected, they will just not be saved between reloads. --- js/Zatacka.js | 35 ++++++++--- js/lib/preferences/PreferenceManager.js | 77 ++++++++++++++++++++++--- js/locales/Zatacka.en_US.js | 1 + js/strings.js | 2 + 4 files changed, 98 insertions(+), 17 deletions(-) diff --git a/js/Zatacka.js b/js/Zatacka.js index 44a6340..b975ed9 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -37,6 +37,7 @@ const Zatacka = ((window, document) => { alt: new WarningMessage(TEXT.hint_alt), ctrl: new WarningMessage(TEXT.hint_ctrl), mouse: new WarningMessage(TEXT.hint_mouse), + preferences_access_denied: new WarningMessage(TEXT.hint_preferences_access_denied), }), defaultPlayers: Object.freeze([ { id: 1, name: "Red" , color: "#FF2800", keyL: KEY["1"] , keyR: KEY.Q }, @@ -188,7 +189,7 @@ const Zatacka = ((window, document) => { function applyCursorBehavior() { const mouseIsBeingUsed = game.getPlayers().some(hasMouseButton); let behavior; - switch (preferenceManager.get(STRINGS.pref_key_cursor)) { + switch (preferenceManager.getCached(STRINGS.pref_key_cursor)) { case STRINGS.pref_value_cursor_hidden_when_mouse_used_by_player: behavior = mouseIsBeingUsed ? STRINGS.cursor_hidden : STRINGS.cursor_visible; break; @@ -356,7 +357,12 @@ const Zatacka = ((window, document) => { function showSettings() { clearTimeout(hintPickTimer); clearTimeout(hintProceedTimer); - guiController.updateSettingsForm(preferenceManager.getAllPreferencesWithValues()); + try { + guiController.updateSettingsForm(preferenceManager.getAllPreferencesWithValues()); + } catch(e) { + guiController.updateSettingsForm(preferenceManager.getAllPreferencesWithDefaultValues()); + handleSettingsAccessError(e); + } removeLobbyEventListeners(); addHideSettingsButtonEventListener(); document.addEventListener("keydown", settingsKeyHandler); @@ -367,9 +373,10 @@ const Zatacka = ((window, document) => { document.removeEventListener("keydown", settingsKeyHandler); addLobbyEventListeners(); guiController.parseSettingsForm().forEach((newSetting) => { - if (preferenceManager.get(newSetting.key) !== newSetting.value) { - // Setting has changed, so it must be updated. + try { preferenceManager.set(newSetting.key, newSetting.value); + } catch(e) { + handleSettingsAccessError(e); } }); applySettings(); @@ -377,10 +384,22 @@ const Zatacka = ((window, document) => { } function applySettings() { - // Edge fix: - setEdgeMode(preferenceManager.get(STRINGS.pref_key_edge_fix)); - // Hints: - guiController.setMessageMode(preferenceManager.get(STRINGS.pref_key_hints)); + try { + // Edge fix: + setEdgeMode(preferenceManager.get(STRINGS.pref_key_edge_fix)); + // Hints: + guiController.setMessageMode(preferenceManager.get(STRINGS.pref_key_hints)); + } catch(e) { + setEdgeMode(preferenceManager.getCached(STRINGS.pref_key_edge_fix)); + guiController.setMessageMode(preferenceManager.getCached(STRINGS.pref_key_hints)); + handleSettingsAccessError(e); + } + } + + function handleSettingsAccessError(error) { + if (error.name === STRINGS.error_name_security) { + guiController.showMessage(config.messages.preferences_access_denied); + } } function clearMessages() { diff --git a/js/lib/preferences/PreferenceManager.js b/js/lib/preferences/PreferenceManager.js index 96c4871..9decd4d 100644 --- a/js/lib/preferences/PreferenceManager.js +++ b/js/lib/preferences/PreferenceManager.js @@ -2,12 +2,37 @@ function PreferenceManager(preferencesData) { const LOCALSTORAGE_PREFIX = "pref_key_"; + const CONSOLE_PREFIX = "[PreferenceManager] "; + const ERROR_NAME_SECURITY = "SecurityError"; // Parse and validate preferences: log("Validating preferences ..."); const PREFERENCES = parsePreferences(preferencesData); log("Done."); + // Initialize cached preference database: + let CACHED_PREFERENCES_WITH_VALUES = getAllPreferencesWithDefaultValues(); + CACHED_PREFERENCES_WITH_VALUES.forEach((preferenceWithValue) => { + const key = preferenceWithValue.preference.key; + try { + preferenceWithValue.value = get(key); + } catch(e) { + logWarning(`Using default value '${preferenceWithValue.preference.getDefaultValue()}' for preference '${key}' since no saved value could be loaded from localStorage.`); + } + }); + + function log(string) { + console.log(CONSOLE_PREFIX + string); + } + + function logWarning(string) { + console.warn(CONSOLE_PREFIX + string); + } + + function logError(string) { + console.error(CONSOLE_PREFIX + string); + } + function parsePreferences(preferencesData) { return preferencesData.map(parsePreference); } @@ -31,8 +56,16 @@ function PreferenceManager(preferencesData) { return PREFERENCES.find((pref) => pref.key === key); } - function getAllPreferencesWithValues() { - return PREFERENCES.map((preference) => new PreferenceWithValue(preference, get(preference.key))); + function getCachedPreference(key) { + return CACHED_PREFERENCES_WITH_VALUES.find((preferenceWithValue) => preferenceWithValue.preference.key === key); + } + + function getAllPreferencesWithValues() { // throws SecurityError + return PREFERENCES.map((preference) => new PreferenceWithValue(preference, getCached(preference.key))); + } + + function getAllPreferencesWithDefaultValues() { + return PREFERENCES.map((preference) => new PreferenceWithValue(preference, preference.getDefaultValue())); } function getKey(pref) { @@ -43,13 +76,13 @@ function PreferenceManager(preferencesData) { return getPreference(key).isValidValue(value); } - function setToDefaultValue(key) { + function setToDefaultValue(key) { // throws SecurityError set(key, getDefaultValue(key)); } function getDefaultValue(key) { if (!preferenceExists(key)) { - throw new Error(`Preference ${key} does not exist.`); + throw new Error(`Preference '${key}' does not exist.`); } return getPreference(key).getDefaultValue(); } @@ -58,7 +91,7 @@ function PreferenceManager(preferencesData) { return LOCALSTORAGE_PREFIX + key; } - function set(key, value) { + function set(key, value) { // throws SecurityError if (!preferenceExists(key)) { throw new Error(`There is no preference with key '${key}'.`); } @@ -66,20 +99,44 @@ function PreferenceManager(preferencesData) { if (!isValidPreferenceValue(key, value)) { pref.invalidValue(value); } else { - log(`Setting preference ${key} to ${value}.`); - localStorage.setItem(LS_prefix(key), pref.constructor.stringify(value)); + log(`Setting preference '${key}' to '${value}'.`); + getCachedPreference(key).value = value; + try { + localStorage.setItem(LS_prefix(key), pref.constructor.stringify(value)); + } catch(e) { + logError(`Failed to save value for preference '${key}' to localStorage. The following error was thrown:\n\n${e}`); + if (e.name === ERROR_NAME_SECURITY) { + throw e; + } + } } } - function get(key) { + function get(key) { // throws SecurityError if (!preferenceExists(key)) { throw new Error(`There is no preference with key '${key}'.`); } const pref = getPreference(key); - const savedValue = localStorage.getItem(LS_prefix(key)); + const defaultValue = pref.getDefaultValue(); + let savedValue; + try { + savedValue = localStorage.getItem(LS_prefix(key)); + } catch(e) { + logError(`Failed to load saved value for preference '${key}' from localStorage. The following error was thrown:\n\n${e}`); + if (e.name === ERROR_NAME_SECURITY) { + throw e; + } else { + logWarning(`Returning the default value for '${key}': '${defaultValue}'`); + return defaultValue; + } + } return isValidPreferenceValue(key, pref.constructor.parse(savedValue)) ? pref.constructor.parse(savedValue) : getDefaultValue(key); } + function getCached(key) { + return getCachedPreference(key).value; + } + function setAllToDefault() { log("Resetting all preferences ..."); PREFERENCES.map(getKey).forEach(setToDefaultValue); @@ -90,9 +147,11 @@ function PreferenceManager(preferencesData) { isValidPreferenceValue, set, get, + getCached, setToDefaultValue, getDefaultValue, getAllPreferencesWithValues, + getAllPreferencesWithDefaultValues, setAllToDefault } } \ No newline at end of file diff --git a/js/locales/Zatacka.en_US.js b/js/locales/Zatacka.en_US.js index aad85fc..8339ebf 100644 --- a/js/locales/Zatacka.en_US.js +++ b/js/locales/Zatacka.en_US.js @@ -14,6 +14,7 @@ const TEXT = (() => { hint_alt: `Alt plus some other keys may cause undesired behavior (e.g. switching windows).`, hint_ctrl: `Ctrl plus some other keys may cause undesired behavior (e.g. closing the tab).`, hint_mouse: `Make sure to keep the mouse cursor inside the browser window.`, + hint_preferences_access_denied: `Could not save/load settings. Access to localStorage denied by the browser.`, keyboard_fullscreen_mac: `${KEY_CMD} + ${KEY_SHIFT} + F`, keyboard_fullscreen_standard: "F11", diff --git a/js/strings.js b/js/strings.js index 4cfcaf2..af50fce 100644 --- a/js/strings.js +++ b/js/strings.js @@ -3,6 +3,8 @@ const STRINGS = (() => Object.freeze({ game_url: "ZATACKA.html", + error_name_security: "SecurityError", + class_hidden: "hidden", class_active: "active", class_description: "description", From aca7f6d0ac4bdd5bbfa521e90722ff73c9689159 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sat, 11 Mar 2017 21:50:06 +0100 Subject: [PATCH 10/14] Improve localStorage error handling PreferenceManager now throws all errors it catches when trying to access localStorage, not just SecurityError. `get` has been replaced with `getSaved` to indicate it tries to get from localStorage. It is recommended that a call to `PreferenceManager.getSaved` is wrapped in a `try` clause and that `PreferenceManager.getCached` is used in the `catch` clause as a fallback. --- js/Zatacka.js | 26 +++++++++++------- js/lib/preferences/PreferenceManager.js | 36 +++++++++++-------------- js/locales/Zatacka.en_US.js | 3 ++- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/js/Zatacka.js b/js/Zatacka.js index b975ed9..5003454 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -38,6 +38,7 @@ const Zatacka = ((window, document) => { ctrl: new WarningMessage(TEXT.hint_ctrl), mouse: new WarningMessage(TEXT.hint_mouse), preferences_access_denied: new WarningMessage(TEXT.hint_preferences_access_denied), + preferences_localstorage_failed: new WarningMessage(TEXT.hint_preferences_localstorage_failed), }), defaultPlayers: Object.freeze([ { id: 1, name: "Red" , color: "#FF2800", keyL: KEY["1"] , keyR: KEY.Q }, @@ -358,9 +359,10 @@ const Zatacka = ((window, document) => { clearTimeout(hintPickTimer); clearTimeout(hintProceedTimer); try { - guiController.updateSettingsForm(preferenceManager.getAllPreferencesWithValues()); + guiController.updateSettingsForm(preferenceManager.getAllPreferencesWithValues_saved()); } catch(e) { - guiController.updateSettingsForm(preferenceManager.getAllPreferencesWithDefaultValues()); + logWarning("Could not load settings from localStorage. Using cached settings instead."); + guiController.updateSettingsForm(preferenceManager.getAllPreferencesWithValues_cached()); handleSettingsAccessError(e); } removeLobbyEventListeners(); @@ -372,13 +374,14 @@ const Zatacka = ((window, document) => { function hideSettings() { document.removeEventListener("keydown", settingsKeyHandler); addLobbyEventListeners(); - guiController.parseSettingsForm().forEach((newSetting) => { - try { + try { + guiController.parseSettingsForm().forEach((newSetting) => { preferenceManager.set(newSetting.key, newSetting.value); - } catch(e) { - handleSettingsAccessError(e); - } - }); + }); + } catch(e) { + logWarning("Could not save settings to localStorage."); + handleSettingsAccessError(e); + } applySettings(); guiController.hideSettings(); } @@ -386,10 +389,11 @@ const Zatacka = ((window, document) => { function applySettings() { try { // Edge fix: - setEdgeMode(preferenceManager.get(STRINGS.pref_key_edge_fix)); + setEdgeMode(preferenceManager.getSaved(STRINGS.pref_key_edge_fix)); // Hints: - guiController.setMessageMode(preferenceManager.get(STRINGS.pref_key_hints)); + guiController.setMessageMode(preferenceManager.getSaved(STRINGS.pref_key_hints)); } catch(e) { + logWarning("Could not load settings from localStorage. Using cached settings instead."); setEdgeMode(preferenceManager.getCached(STRINGS.pref_key_edge_fix)); guiController.setMessageMode(preferenceManager.getCached(STRINGS.pref_key_hints)); handleSettingsAccessError(e); @@ -399,6 +403,8 @@ const Zatacka = ((window, document) => { function handleSettingsAccessError(error) { if (error.name === STRINGS.error_name_security) { guiController.showMessage(config.messages.preferences_access_denied); + } else { + guiController.showMessage(config.messages.preferences_localstorage_failed); } } diff --git a/js/lib/preferences/PreferenceManager.js b/js/lib/preferences/PreferenceManager.js index 9decd4d..4bb0460 100644 --- a/js/lib/preferences/PreferenceManager.js +++ b/js/lib/preferences/PreferenceManager.js @@ -3,7 +3,6 @@ function PreferenceManager(preferencesData) { const LOCALSTORAGE_PREFIX = "pref_key_"; const CONSOLE_PREFIX = "[PreferenceManager] "; - const ERROR_NAME_SECURITY = "SecurityError"; // Parse and validate preferences: log("Validating preferences ..."); @@ -15,7 +14,7 @@ function PreferenceManager(preferencesData) { CACHED_PREFERENCES_WITH_VALUES.forEach((preferenceWithValue) => { const key = preferenceWithValue.preference.key; try { - preferenceWithValue.value = get(key); + preferenceWithValue.value = getSaved(key); } catch(e) { logWarning(`Using default value '${preferenceWithValue.preference.getDefaultValue()}' for preference '${key}' since no saved value could be loaded from localStorage.`); } @@ -60,7 +59,11 @@ function PreferenceManager(preferencesData) { return CACHED_PREFERENCES_WITH_VALUES.find((preferenceWithValue) => preferenceWithValue.preference.key === key); } - function getAllPreferencesWithValues() { // throws SecurityError + function getAllPreferencesWithValues_saved() { // throws SecurityError etc + return PREFERENCES.map((preference) => new PreferenceWithValue(preference, getSaved(preference.key))); + } + + function getAllPreferencesWithValues_cached() { return PREFERENCES.map((preference) => new PreferenceWithValue(preference, getCached(preference.key))); } @@ -76,7 +79,7 @@ function PreferenceManager(preferencesData) { return getPreference(key).isValidValue(value); } - function setToDefaultValue(key) { // throws SecurityError + function setToDefaultValue(key) { // throws SecurityError etc set(key, getDefaultValue(key)); } @@ -91,7 +94,7 @@ function PreferenceManager(preferencesData) { return LOCALSTORAGE_PREFIX + key; } - function set(key, value) { // throws SecurityError + function set(key, value) { // throws SecurityError etc if (!preferenceExists(key)) { throw new Error(`There is no preference with key '${key}'.`); } @@ -104,31 +107,23 @@ function PreferenceManager(preferencesData) { try { localStorage.setItem(LS_prefix(key), pref.constructor.stringify(value)); } catch(e) { - logError(`Failed to save value for preference '${key}' to localStorage. The following error was thrown:\n\n${e}`); - if (e.name === ERROR_NAME_SECURITY) { - throw e; - } + logError(`Failed to save value for preference '${key}' to localStorage. The following error was thrown:\n${e}`); + throw e; // likely a SecurityError, but could be others as well } } } - function get(key) { // throws SecurityError + function getSaved(key) { // throws SecurityError etc if (!preferenceExists(key)) { throw new Error(`There is no preference with key '${key}'.`); } const pref = getPreference(key); - const defaultValue = pref.getDefaultValue(); let savedValue; try { savedValue = localStorage.getItem(LS_prefix(key)); } catch(e) { - logError(`Failed to load saved value for preference '${key}' from localStorage. The following error was thrown:\n\n${e}`); - if (e.name === ERROR_NAME_SECURITY) { - throw e; - } else { - logWarning(`Returning the default value for '${key}': '${defaultValue}'`); - return defaultValue; - } + logError(`Failed to load saved value for preference '${key}' from localStorage. The following error was thrown:\n${e}`); + throw e; // likely a SecurityError, but could be others as well } return isValidPreferenceValue(key, pref.constructor.parse(savedValue)) ? pref.constructor.parse(savedValue) : getDefaultValue(key); } @@ -146,11 +141,12 @@ function PreferenceManager(preferencesData) { return { isValidPreferenceValue, set, - get, + getSaved, getCached, setToDefaultValue, getDefaultValue, - getAllPreferencesWithValues, + getAllPreferencesWithValues_saved, + getAllPreferencesWithValues_cached, getAllPreferencesWithDefaultValues, setAllToDefault } diff --git a/js/locales/Zatacka.en_US.js b/js/locales/Zatacka.en_US.js index 8339ebf..b4b9bad 100644 --- a/js/locales/Zatacka.en_US.js +++ b/js/locales/Zatacka.en_US.js @@ -14,7 +14,8 @@ const TEXT = (() => { hint_alt: `Alt plus some other keys may cause undesired behavior (e.g. switching windows).`, hint_ctrl: `Ctrl plus some other keys may cause undesired behavior (e.g. closing the tab).`, hint_mouse: `Make sure to keep the mouse cursor inside the browser window.`, - hint_preferences_access_denied: `Could not save/load settings. Access to localStorage denied by the browser.`, + hint_preferences_access_denied: `Could not save/load settings because access to localStorage was denied by the browser. This might be caused by "third-party site data" being blocked or similar.`, + hint_preferences_localstorage_failed: `Could not save/load settings because access to localStorage failed.`, keyboard_fullscreen_mac: `${KEY_CMD} + ${KEY_SHIFT} + F`, keyboard_fullscreen_standard: "F11", From b8d89b3fa319119b5239bfd167a192edd7bcd308 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sat, 11 Mar 2017 22:01:58 +0100 Subject: [PATCH 11/14] Fix stupid redundant line --- js/Zatacka.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/Zatacka.js b/js/Zatacka.js index 5003454..e4f471f 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -345,7 +345,7 @@ const Zatacka = ((window, document) => { // A simple trick to prevent accidental unloading of the entire game. const message = TEXT.hint_unload; event.returnValue = message; // Gecko, Trident, Chrome 34+ - return TEXT.hint_unload; // Gecko, Webkit, Chrome <34 + return message; // Gecko, Webkit, Chrome <34 } function settingsKeyHandler(event) { From de88aa31aeb4d504477d6012c457ec3599d856e0 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sun, 12 Mar 2017 00:31:25 +0100 Subject: [PATCH 12/14] Fix bug causing only the first setting to be saved I messed up so that, when parsing the settings form, if an error occurred, subsequent settings would not be parsed. Thus, if access to localStorage was denied, only the first setting would be saved. This commit fixes that issue. --- js/Zatacka.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/js/Zatacka.js b/js/Zatacka.js index e4f471f..92ab933 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -374,14 +374,14 @@ const Zatacka = ((window, document) => { function hideSettings() { document.removeEventListener("keydown", settingsKeyHandler); addLobbyEventListeners(); - try { - guiController.parseSettingsForm().forEach((newSetting) => { + guiController.parseSettingsForm().forEach((newSetting) => { + try { preferenceManager.set(newSetting.key, newSetting.value); - }); - } catch(e) { - logWarning("Could not save settings to localStorage."); - handleSettingsAccessError(e); - } + } catch(e) { + logWarning(`Could not save setting '${newSetting.key}' to localStorage.`); + handleSettingsAccessError(e); + } + }); applySettings(); guiController.hideSettings(); } From 723706e6c7f10c68f5c0b0a9cd92d6a2d9df90bd Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sun, 12 Mar 2017 01:14:45 +0100 Subject: [PATCH 13/14] Add Browser Safe Mode The BSM means that Kurve is run in its own window without history to avoid mid-game disruptions caused by user mistakes. It relies on `window.open()`. If popups are blocked, we show a fallback message with a link to click instead. --- index.html | 1 + js/SplashScreen.js | 13 ++++++++++++- js/locales/Zatacka.en_US.js | 1 + js/strings.js | 1 + kurve.se.css | 8 ++++++-- 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 089a101..b4f78c4 100644 --- a/index.html +++ b/index.html @@ -64,6 +64,7 @@

Achtung, die Kurve!

+ diff --git a/js/SplashScreen.js b/js/SplashScreen.js index 5abd904..c3d13c7 100644 --- a/js/SplashScreen.js +++ b/js/SplashScreen.js @@ -8,7 +8,11 @@ } function proceedToGame() { - document.location.href = STRINGS.game_url; + const newWindow = window.open(STRINGS.game_url); + if (!newWindow || newWindow.closed || typeof newWindow.closed === "undefined") { + // Browser is blocking popups. + showPopupHint(); + } } function splashScreenKeyHandler(event) { @@ -32,6 +36,13 @@ } } + function showPopupHint() { + const popupHintElement = byID(STRINGS.id_popup_hint); + if (isHTMLElement(popupHintElement)) { + popupHintElement.innerHTML = TEXT.hint_popup; + } + } + function addEventListeners() { document.addEventListener("keydown", splashScreenKeyHandler); document.addEventListener("DOMContentLoaded", showStartHint); diff --git a/js/locales/Zatacka.en_US.js b/js/locales/Zatacka.en_US.js index b4b9bad..b6d706f 100644 --- a/js/locales/Zatacka.en_US.js +++ b/js/locales/Zatacka.en_US.js @@ -7,6 +7,7 @@ const TEXT = (() => { return Object.freeze({ hint_unload: `Are you sure you want to unload the page?`, hint_start: `Press Space to start`, + hint_popup: `It is recommended to run Kurve in its own window without history (to avoid switching tabs or navigating back in history mid-game). To do that, please allow popups or click here.`, hint_pick: `Pick your desired color by pressing the corresponding LEFT key (e.g. M for Orange).`, hint_proceed: `Press Space or Enter to start!`, hint_next: `Press Space or Enter to proceed, or Esc to quit.`, diff --git a/js/strings.js b/js/strings.js index af50fce..e625383 100644 --- a/js/strings.js +++ b/js/strings.js @@ -18,6 +18,7 @@ const STRINGS = (() => Object.freeze({ id_start_hint: "start-hint", id_fullscreen_hint: "fullscreen-hint", + id_popup_hint: "popup-hint", pref_key_cursor: "cursor", pref_value_cursor_always_visible: "always_visible", diff --git a/kurve.se.css b/kurve.se.css index 07704ba..c61d09f 100644 --- a/kurve.se.css +++ b/kurve.se.css @@ -27,13 +27,17 @@ main { font-size: 1.2em; } -#fullscreen-hint { +#fullscreen-hint, #popup-hint { font-size: 0.9em; } +#popup-hint a { + text-decoration: underline; +} + main footer { bottom: 0; - height: 80px; + height: 40px; position: absolute; width: 100%; } From 1f06682813e37c8da92b77b141ff68cc4ce8b46c Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sun, 12 Mar 2017 12:14:40 +0100 Subject: [PATCH 14/14] Fix settings parsing bug in some browsers --- js/GUIController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/GUIController.js b/js/GUIController.js index 50c9b43..40c8cc9 100644 --- a/js/GUIController.js +++ b/js/GUIController.js @@ -209,7 +209,7 @@ function GUIController(cfg) { const newSettings = []; // elements: const inputs = settingsForm.querySelectorAll("input"); - inputs.forEach((input) => { + Array.from(inputs).forEach((input) => { if (input.type === "checkbox") { // checkbox newSettings.push({ key: input.dataset.key, value: input.checked }); @@ -225,7 +225,7 @@ function GUIController(cfg) { }); //