// RestoreManager.js
(function () {
/*global adventurejs A aria*/
/**
*
* @class adventurejs.RestoreManager
* @ajsinternal
* @ajsnavheading FrameworkReference
* @param {Game} game A reference to the game instance.
* @summary Manages the process of restoring saved games.
* @todo Restore from adventurejs.com web server.
* @classdesc
* <p>
* <strong>RestoreManager</strong> manages the job of restoring
* saved games. It contains all the methods needed to create the
* Restore pop-up screen. RestoreManager can restore a game from
* a local save file, from browser cookies, or from the
* <a href="https://adventurejs.com">adventurejs.com</a>
* web server.
* </p>
* <p>
* RestoreManager is created automatically
* by {@link adventurejs.Game|Game}. This is an internal class that
* authors should not need to construct or modify. However, if
* you'd like to try, you can find styles for the Save & Restore
* pop-ups in <a href="/css/adventurejs.css">adventurejs.css</a>.
* All relevant styles are prefixed with '.save_' or '.restore_'.
* </p>
*/
class RestoreManager {
constructor(game) {
/**
* A reference back to the main {@link adventurejs.Game|Game} object.
* @var {Object} adventurejs.RestoreManager#game
* @default {}
*/
this.game = game;
/**
* Collection of HTML elements: the tabs used to navigate
* between the different restore methods.
* @var {Array} adventurejs.RestoreManager#restoreTabs
* @default []
*/
this.restoreTabs = [];
/**
* Collection of HTML elements: the panes containing the
* different restore methods.
* @var {Array} adventurejs.RestoreManager#restorePanes
* @default []
*/
this.restorePanes = [];
/**
* Collection of HTML elements: the action buttons for
* the different restore methods.
* @var {Array} adventurejs.RestoreManager#restoreButtons
* @default []
*/
this.restoreButtons = [];
/**
* Div element to contain the Restore pop-up.
* @var {HTMLElement} adventurejs.RestoreManager#restore_dialog
* @default {}
*/
this.restore_dialog = document.createElement("div");
this.restore_dialog.classList.add("ajs-restore-dialog", "ajs-dialog");
this.restore_dialog.setAttribute("aria-hidden", true);
this.restore_dialog.setAttribute("aria-modal", true);
this.restore_dialog.role = "dialog";
this.restore_dialog.tabIndex = "1";
this.restore_dialog.style.display = "none";
this.restore_dialog.setAttribute(
"aria-labelledby",
`${this.game.game_name}-restore-dialog-title`
);
this.game.display.dialogsEl.appendChild(this.restore_dialog);
// Source HTML is stored in restoreManager.html
this.restore_dialog.innerHTML = `
<div class="ajs-restore-dialog-outer ajs-dialog-outer">
<div class="ajs-restore-dialog-inner ajs-dialog-inner">
<h2 id="${this.game.game_name}-restore-dialog-title" class="ajs-restore-dialog-title ajs-dialog-title">RESTORE GAME</h2>
<div class="ajs-restore-dialog-content ajs-dialog-content">
<div id="${this.game.game_name}-restore-dialog-radiogroup" role="radiogroup" aria-labelledby="${this.game.game_name}-restore-dialog-radiogroup-label">
<p id="${this.game.game_name}-restore-dialog-radiogroup-label" class="ajs-hidden-label">Select an option:</p>
<div class="ajs-dialog-radio-tab-container">
<label class="ajs-dialog-radio-tab">
<input
type="radio"
name="restoreoptions"
value="restore_from_file"
data-type="file"
aria-describedby="${this.game.game_name}-restore-dialog-descriptions"
checked
/>
Restore from File
</label>
<label class="ajs-dialog-radio-tab">
<input
type="radio"
name="restoreoptions"
value="restore_from_browser"
data-type="browser"
aria-describedby="${this.game.game_name}-restore-dialog-descriptions"
/>
Restore from Browser
</label>
<label class="ajs-dialog-radio-tab">
<input
type="radio"
name="restoreoptions"
value="restore_from_server"
data-type="server"
aria-describedby="${this.game.game_name}-restore-dialog-descriptions"
/>
Restore from Server
</label>
</div>
<!-- /ajs-dialog-radio-tab-container -->
</div>
<!-- /radiogroup -->
<!-- CONTEXTUAL DESCRIPTIONS -->
<div id="${this.game.game_name}-restore-dialog-descriptions" class="ajs-dialog-option-descriptions" aria-live="polite">
<!-- Select an option to see more details. -->
</div>
<!-- SELECTION OPTIONS -->
<div id="${this.game.game_name}-restore-dialog-selections" class="ajs-dialog-option-selections">
<!-- CHOOSE FILE FROM DRIVE -->
<div
id="${this.game.game_name}-restore-from-file-selection"
class="ajs-dialog-option-selection ajs-restore-from-file-input-container "
data-type="file"
>
<input
id="${this.game.game_name}-restore-from-file-input"
class="ajs-restore-from-file-input ajs-file-reader-required"
type="file"
accept=".json"
name="restore-file-input"
>
<div class="ajs-file-reader-unsupported">
:-(
</div>
</div>
<!-- CHOOSE FROM BROWSER -->
<div
id="${this.game.game_name}-restore-from-browser-selection"
class="ajs-dialog-option-selection ajs-restore-from-browser-listbox-container"
data-type="browser"
>
<p
id="${this.game.game_name}-restore-from-listbox-label"
class="ajs-restore-from-browser-listbox-label ajs-listbox-label"
>
Select a saved game to restore:
</p>
<ul
class="ajs-restore-browser-listbox ajs-listbox listbox"
id="${this.game.game_name}-restore-from-browser-listbox"
aria-labelledby="${this.game.game_name}-restore-from-listbox-label"
role="listbox"
tabindex="0"
>
</ul>
</div>
<!-- CHOOSE FROM SERVER -->
<div
id="${this.game.game_name}-restore-from-server-selection"
class="ajs-dialog-option-selection ajs-restore-server-container"
data-type="server"
>
<p class="ajs-restore-from-server-label">
TBD
</p>
</div>
</div>
<!-- DIALOG BUTTONS -->
<div class="ajs-restore-dialog-buttons ajs-dialog-button-container">
<!-- CANCEL BUTTON -->
<button
id="${this.game.game_name}-restore-dialog-cancel-button"
class="ajs-restore-dialog-cancel-button ajs-cancel-button ajs-dialog-button button-secondary"
type="button"
value="Cancel"
name="restore_cancel"
>
Cancel
</button>
<!-- RESTORE BUTTON -->
<button
id="${this.game.game_name}-restore-dialog-submit-button"
class="ajs-restore-dialog-submit-button ajs-restore-button ajs-dialog-button button-primary"
type="button"
value="submit"
name="restore_submit"
>
Restore
</button>
</div>
<!-- /ajs-restore-dialog-buttons -->
</div>
<!-- /ajs-restore-dialog-content -->
</div>
<!-- /ajs-restore-dialog-inner -->
</div>
<!-- /ajs-restore-dialog-outer -->
`;
// --------------------------------------------------
// RADIO BUTTONS
// --------------------------------------------------
this.radiogroup = this.game.display.displayEl.querySelector(
`#${this.game.game_name}-restore-dialog-radiogroup`
);
const radios = this.game.display.displayEl.querySelectorAll(
'input[name="restoreoptions"]'
);
const descriptions = this.game.display.displayEl.querySelector(
`#${this.game.game_name}-restore-dialog-descriptions`
);
const selections = this.game.display.displayEl.querySelectorAll(
`.ajs-dialog-option-selection`
);
const optionDescriptions = {
restore_from_file: `
<div class="ajs-dialog-option-description">
<p class="">
Use this option to restore a saved game file that you
downloaded using the
<span class="ajs-dialog-emphasis">Save to File</span>
option. If, when you saved, your browser didn't offer
you a file dialog to choose your download location,
then your save file was probably downloaded to
your computer's default Download folder.
</p>
<div
class="ajs-file-reader-unsupported"
>
<div class="alert alert-danger">
<p class="">
Unfortunately, your web browser doesn't appear to support this
save/restore method. Please try one of the other methods, or try
playing with a different web browser.
</p>
</div>
</div>
</div>
`,
restore_from_browser: `
<div class="ajs-dialog-option-description">
<p class="">
Use this option to restore a saved game you saved
to the browser using the
<span class="ajs-dialog-emphasis">Save to Browser</span>
option. It will only find save games you made while
playing the game
on this browser on this computer at this web domain.
</p>
</div>
`,
restore_from_server: `
<div class="ajs-dialog-option-description">
<p class="">
Use this option to restore a saved game you saved using the
<span class="ajs-dialog-emphasis">Save to Server</span>
option. You will need to log in to the adventurejs server
if you haven't already.
</p>
</div>
`,
};
descriptions.innerHTML =
optionDescriptions[Object.keys(optionDescriptions)[0]];
radios.forEach((radio) => {
radio.selection = this.game.display.displayEl.querySelector(
`[data-type="${radio.dataset.type}"].ajs-dialog-option-selection`
);
radio.addEventListener("change", (event) => {
const selectedOption = event.target.value;
console.warn(selectedOption);
descriptions.innerHTML =
optionDescriptions[selectedOption] ||
"Select an option to see more details.";
// set selection
selections.forEach((selection) => {
selection.classList.remove("active");
});
event.target.selection.classList.add("active");
});
});
setTimeout(() => {
const radioButtons = Array.from(
this.radiogroup.querySelectorAll('input[type="radio"]')
);
// Find the checked radio button
const checkedRadio = radioButtons.find((radio) => radio.checked);
// If no radio button is checked, check the first one
if (!checkedRadio && this.radiogroup.length > 0) {
// Check the first radio button
this.radiogroup[0].checked = true;
// Apply the "active" class to the associated selection tool
selections[0].classList.add("active");
console.log(
"No radio button was selected. The first one is now checked."
);
} else if (checkedRadio) {
checkedRadio.selection.classList.add("active");
}
}, 50);
// --------------------------------------------------
// CANCEL BUTTON
// --------------------------------------------------
/**
* Button element to dispel the Restore dialog.
* @var {HTMLElement} adventurejs.SaveManager#restore_cancel
* @default {}
*/
this.restore_cancel = this.game.display.displayEl.querySelector(
`#${this.game.game_name}-restore-dialog-cancel-button`
);
this.restore_cancel.manager = this;
this.restore_cancel.addEventListener("click", function () {
this.manager.clickClose();
});
// --------------------------------------------------
// RESTORE BUTTON
// --------------------------------------------------
this.restore_submit = this.game.display.displayEl.querySelector(
`#${this.game.game_name}-restore-dialog-submit-button`
);
this.restore_submit.manager = this;
this.restore_submit.addEventListener("click", function () {
this.manager.clickSubmit();
});
// --------------------------------------------------
// CHOOSE FILE FROM DRIVE
// --------------------------------------------------
this.restore_file_input = this.game.display.displayEl.querySelector(
`#${this.game.game_name}-restore-from-file-input`
);
this.restore_file_input.manager = this;
this.restore_file_input.addEventListener(
"change",
function (e) {
console.log("restore_file_input.click");
console.log("--", e.target.files[0]);
console.log("--", e.target.value);
// we've set our file input to accept=".json", but just in case
if (".json" !== e.target.value.substring(e.target.value.length - 5)) {
e.target.value = "";
alert("Selected file is not a JSON file.");
return false;
}
this.manager.restore_submit.classList.remove("inactive");
},
false
);
// --------------------------------------------------
// BROWSER PANE
// --------------------------------------------------
// LISTBOX
// example with aria tagging
// https://www.w3.org/TR/wai-aria-practices/examples/listbox/listbox-scrollable.html
// instantiate a new listbox
// var exListbox = new aria.Listbox(document.getElementById('ss_elem_list'));
this.restore_from_browser_listbox =
this.game.display.displayEl.querySelector(
`#${this.game.game_name}-restore-from-browser-listbox`
);
this.restore_from_browser_observer = new MutationObserver(function (
mutation
) {
if (mutation[0].attributeName !== "aria-activedescendant") {
return;
}
var selection = mutation[0].target.getAttribute(
mutation[0].attributeName
);
//if( document.getElementById( selection ).classList.contains( "focused" ) ) {
if (
null !==
document.getElementById(selection).getAttribute("aria-selected")
) {
console.log(selection);
this.manager.restore_submit.classList.remove("inactive");
}
});
this.restore_from_browser_observer.manager = this;
this.restore_from_browser_observer.observe(
this.restore_from_browser_listbox,
{
// childList: true,
attributes: true,
}
);
this.handleEscape = this.handleEscape.bind(this); // Bind once
//
} // CONSTRUCTOR
/**
* Open the Restore modal dialog.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#openDialog
* @kind function
*/
openDialog() {
this.resetDialog();
// deactivate main content
document.activeElement.blur();
this.game.display.contentEl.blur();
this.game.display.contentEl.setAttribute("aria-hidden", true);
this.game.display.contentEl.setAttribute("inert", "");
// activate dialog
this.restore_dialog.style.display = "block";
this.restore_dialog.classList.add("active");
this.restore_dialog.setAttribute("aria-hidden", false);
this.restore_dialog.focus();
document.addEventListener("keyup", this.handleEscape);
}
/**
* Reset the Restore modal dialog.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#resetDialog
* @kind function
*/
resetDialog() {
this.restore_file_input.value = "";
this.restore_submit.classList.add("inactive");
this.getLocalStorageList();
}
/**
* Close the Restore modal dialog.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#closeDialog
* @kind function
*/
closeDialog() {
document.removeEventListener("keyup", this.handleEscape);
document.activeElement.blur();
this.restore_dialog.classList.remove("active");
this.restore_dialog.setAttribute("aria-hidden", true);
this.restoreButtons.forEach(function (button) {
button.classList.add("inactive");
});
setTimeout(() => {
this.restore_dialog.style.display = "none";
// set focus on input
this.game.display.contentEl.setAttribute("aria-hidden", false);
this.game.display.contentEl.removeAttribute("inert");
this.game.display.inputEl.focus();
}, 250);
}
/**
* Function that closes the dialog on escape keyup.
* @param {*} event
*/
handleEscape(event) {
var key = event.which || event.keyCode;
if (key === 27) {
event.stopPropagation();
this.clickClose(this);
}
}
/**
* Function that gets called by the Close button.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#clickClose
* @kind function
*/
clickClose() {
console.log("clickClose");
this.closeDialog();
var msg = "Restore cancelled.";
this.game.print(msg);
return;
}
/**
* Restore the selected save file.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#restoreFromFile
* @kind function
*/
restoreFromFile() {
console.log("restoreFromFile");
var fileReader = new FileReader();
// forward closured fileReader.onload event
// to a more useful scope
fileReader.onload = (function (manager) {
return function (e) {
manager.fileReaderOnload(e);
};
})(this);
fileReader.readAsText(this.restore_file_input.files[0], "UTF-8");
}
/**
* Complete the restore operation and close the pop-up.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#fileReaderOnload
* @kind function
*/
fileReaderOnload(e) {
console.log("fileReaderOnload");
console.log(e);
this.closeDialog();
var restored = A.restoreWorld.call(this.game, e.target.result);
if (false === restored) {
var msg = "Restore failed!";
this.game.print(msg);
return false;
}
var msg = "Game restored.";
this.game.print(msg);
return;
}
// -----------------
// BROWSER PANE
// -----------------
// https://blog.logrocket.com/the-complete-guide-to-using-localstorage-in-javascript-apps-ba44edb53a36/
/**
* Restore the selected save file.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#restoreFromBrowser
* @kind function
*/
restoreFromBrowser() {
console.log("restoreFromBrowser");
var saveName = this.restore_from_browser_listbox.getAttribute(
"aria-activedescendant"
);
var restored = this.game.restoreWorld(window.localStorage[saveName]);
this.closeDialog();
if (!restored) {
var msg = "Restore failed!";
this.game.print(msg);
return false;
}
var msg = "Game restored.";
this.game.print(msg);
return;
}
/**
* Get a list of saved games stored in browser cookies.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#getLocalStorageList
* @kind function
*/
getLocalStorageList() {
this.resetLocalStorageListBox();
var storage = window.localStorage;
for (var key in storage) {
// if( this.game.titleSerialized !== key.substr( 0, this.game.titleSerialized.length ) ) // @deprecated
if (
this.game.titleSerialized !==
key.substring(0, this.game.titleSerialized.length)
) {
continue;
}
var li = document.createElement("li");
li.id = key;
//li.innerHTML = key;
li.innerHTML = key.substring(this.game.titleSerialized.length + 1);
li.setAttribute("role", "option");
this.restore_from_browser_listbox.appendChild(li);
}
if (0 < this.restore_from_browser_listbox.childNodes.length) {
this.restore_from_browser_listbox.setAttribute(
"aria-activedescendant",
this.restore_from_browser_listbox.childNodes[0].id
);
}
this.restoreListboxInstance_Browser = new aria.Listbox(
this.restore_from_browser_listbox
);
this.restore_from_browser_listbox.focus();
// TODO why doesn't this focus work?
}
/**
* Reset local storage listbox.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#resetLocalStorageListBox
* @kind function
*/
resetLocalStorageListBox() {
if (
!this.restore_from_browser_listbox.innerHTML &&
"undefined" === typeof this.restoreListboxInstance_Browser
) {
// already empty
return;
}
if ("undefined" !== typeof this.restoreListboxInstance_Browser) {
delete this.restoreListboxInstance_Browser;
}
for (
var i = this.restore_from_browser_listbox.childNodes.length - 1;
i > -1;
i--
) {
// TODO remove? or repurpose?
this.restore_from_browser_listbox.removeChild(
this.restore_from_browser_listbox.childNodes[i]
);
}
this.restore_from_browser_listbox.innerHTML = "";
return;
}
// -----------------
// SERVER PANE
// -----------------
/**
* Restore the seleted save file.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#restoreFromServer
* @kind function
*/
restoreFromServer() {
console.log("restoreFromServer");
}
/**
* Make a selected tab active.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#selectTab
* @kind function
*/
selectTab(selectedTab) {
this.restoreTabs.forEach(function (tabButton) {
if (tabButton !== selectedTab) {
tabButton.classList.remove("active");
tabButton.restorePane.classList.remove("active");
tabButton.restorePaneQA.classList.remove("active");
} else {
tabButton.classList.add("active");
tabButton.restorePane.classList.add("active");
}
}, selectedTab);
}
/**
* Provides a chainable shortcut method for setting a number of properties on the instance.
* @memberOf adventurejs.RestoreManager
* @method adventurejs.RestoreManager#set
* @param {Object} props A generic object containing properties to copy to the DisplayObject instance.
* @returns {adventurejs.RestoreManager} Returns the instance the method is called on (useful for chaining calls.)
* @chainable
*/
set(props) {
// if (props != null) {
// for (var n in props) {
// this[n] = props[n];
// }
// }
// return this;
return A.deepSet.call(this.game, props, this);
}
}
adventurejs.RestoreManager = RestoreManager;
})();