// SaveManager.js
// https://eligrey.com/demos/FileSaver.js/
// https://www.dropzonejs.com/
// https://gist.github.com/liabru/11263260#file-save-file-local-js
(function () {
/*global adventurejs A*/
"use strict";
/**
*
* @class adventurejs.SaveManager
* @ajsnavheading FrameworkReference
* @param {Game} game A reference to the game instance.
* @summary Manages the process of saving games.
* @todo Save to adventurejs.com web server.
* @classdesc
* <p>
* <strong>SaveManager</strong> manages the job of saving games.
* It contains all the methods needed to create the
* Save pop-up screen. SaveManager can save a game to
* a local save file, to browser cookies, or to the
* <a href="https://adventurejs.com">adventurejs.com</a>
* web server.
* </p>
* <p>
* SaveManager 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 SaveManager {
constructor(game) {
/**
* A reference back to the main {@link adventurejs.Game|Game} object.
* @var {Object} adventurejs.SaveManager#game
* @default {}
*/
this.game = game;
/**
* Collection of HTML elements: the tabs used to navigate
* between the different save methods.
* @var {Array} adventurejs.SaveManager#saveTabs
* @default []
*/
this.saveTabs = [];
/**
* Collection of HTML elements: the panes containing the
* different save methods.
* @var {Array} adventurejs.SaveManager#savePanes
* @default []
*/
this.savePanes = [];
/**
* Collection of HTML elements: the action buttons for
* the different save methods.
* @var {Array} adventurejs.SaveManager#saveButtons
* @default []
*/
this.saveButtons = [];
/**
* Div element to contain the Save pop-up.
* @var {HTMLElement} adventurejs.RestoreManager#saveDisplay
* @default {}
*/
this.saveDisplay = document.createElement("div");
this.saveDisplay.classList.add("ajs-save-display");
this.game.display.displayEl.appendChild(this.saveDisplay);
this.saveOuter = document.createElement("div");
this.saveOuter.classList.add("save_outer");
this.saveDisplay.appendChild(this.saveOuter);
this.saveInner = document.createElement("div");
this.saveInner.classList.add("save_inner");
this.saveOuter.appendChild(this.saveInner);
// -----------------
// CLOSE BUTTON
// -----------------
/**
* Button element to close the Save pop-up.
* @var {HTMLElement} adventurejs.SaveManager#restoreClose
* @default {}
*/
this.saveClose = document.createElement("button");
this.saveClose.classList.add("save_close", "close_button");
this.saveClose.type = "button";
this.saveClose.value = "Close";
this.saveClose.innerHTML = "X";
this.saveClose.name = "saveClose";
this.saveClose.manager = this;
this.saveInner.appendChild(this.saveClose);
this.saveClose.addEventListener("click", function () {
this.manager.clickButton_Close();
});
// -----------------
// TITLE BAR
// -----------------
/**
* P element to close the Save pop-up's title bar.
* @var {HTMLElement} adventurejs.SaveManager#saveDisplayTitle
* @default {}
*/
this.saveDisplayTitle = document.createElement("p");
this.saveDisplayTitle.classList.add("save_display_title");
this.saveDisplayTitle.innerHTML = "SAVE GAME";
this.saveInner.appendChild(this.saveDisplayTitle);
// -----------------
// FILENAME INPUT
// -----------------
/**
* Div element to contain the Save row input.
* @var {HTMLElement} adventurejs.SaveManager#saveRowInput
* @default {}
*/
this.saveRowInput = document.createElement("div");
this.saveRowInput.classList.add("save_row_input");
this.saveInner.appendChild(this.saveRowInput);
this.saveInputLabel = document.createElement("p");
this.saveInputLabel.classList.add("save_input_label");
this.saveInputLabel.innerHTML = "Enter a name for your save file:";
this.saveRowInput.appendChild(this.saveInputLabel);
this.saveInput = document.createElement("input");
this.saveInput.setAttribute("id", this.game.name + "_" + "save_input");
this.saveInput.classList.add("save_input");
this.saveInput.pattern = "[_A-Za-z0-9\\-]{64}";
this.saveInput.manager = this;
this.saveRowInput.appendChild(this.saveInput);
this.saveInput.addEventListener("input", function () {
this.manager.saveInputOninput(this);
});
this.saveInput.addEventListener("onkeydown", function () {
this.manager.saveInputOninput(this);
});
// -----------------
// TABS
// -----------------
/**
* Div element to contain the Save pop-up's tabs.
* @var {HTMLElement} adventurejs.SaveManager#saveRowTabs
* @default {}
*/
this.saveRowTabs = document.createElement("div");
this.saveRowTabs.classList.add("save_row_tabs");
this.saveInner.appendChild(this.saveRowTabs);
// -----------------
// FILE TAB
// -----------------
/**
* Button element to navigate to 'Save to File' option.
* @var {HTMLElement} adventurejs.SaveManager#saveTab_File
* @default {}
*/
this.saveTab_File = document.createElement("button");
this.saveTab_File.classList.add("save_tab_file", "active", "save_tab");
this.saveTab_File.type = "button";
this.saveTab_File.value = "Save to File";
this.saveTab_File.innerHTML = "Save to File";
this.saveTab_File.name = "saveTab_File";
this.saveTab_File.manager = this;
this.saveRowTabs.appendChild(this.saveTab_File);
this.saveTab_File.addEventListener("click", function () {
if (false === this.classList.contains("active")) {
this.manager.selectTab(this);
//this.manager.clickSaveButton_File();
}
});
this.saveTabs.push(this.saveTab_File);
// -----------------
// BROWSER TAB
// -----------------
/**
* Button element to navigate to 'Save to Browser' option.
* @var {HTMLElement} adventurejs.SaveManager#saveTab_Browser
* @default {}
*/
this.saveTab_Browser = document.createElement("button");
this.saveTab_Browser.classList.add("save_tab_browser", "save_tab");
this.saveTab_Browser.type = "button";
this.saveTab_Browser.value = "Save to Browser";
this.saveTab_Browser.innerHTML = "Save to Browser";
this.saveTab_Browser.name = "saveTab_Browser";
this.saveTab_Browser.manager = this;
this.saveRowTabs.appendChild(this.saveTab_Browser);
this.saveTab_Browser.addEventListener("click", function () {
if (false === this.classList.contains("active")) {
this.manager.selectTab(this);
//this.manager.clickButton_Browser();
}
});
this.saveTabs.push(this.saveTab_Browser);
// -----------------
// SERVER TAB
// -----------------
/**
* Button element to navigate to 'Save to Server' option.
* @var {HTMLElement} adventurejs.SaveManager#saveTab_Server
* @default {}
*/
this.saveTab_Server = document.createElement("button");
this.saveTab_Server.classList.add("save_tab_server", "save_tab");
this.saveTab_Server.type = "button";
this.saveTab_Server.value = "Save Server";
this.saveTab_Server.innerHTML = "Save to Server";
this.saveTab_Server.name = "saveTab_Server";
this.saveTab_Server.manager = this;
this.saveRowTabs.appendChild(this.saveTab_Server);
this.saveTab_Server.addEventListener("click", function () {
if (false === this.classList.contains("active")) {
this.manager.selectTab(this);
//this.manager.clickButton_Server();
}
});
this.saveTabs.push(this.saveTab_Server);
// -----------------
// PANES
// -----------------
/**
* Div element to contain Save panes.
* @var {HTMLElement} adventurejs.SaveManager#saveRowPanes
* @default {}
*/
this.saveRowPanes = document.createElement("div");
this.saveRowPanes.classList.add("save_row_panes");
this.saveInner.appendChild(this.saveRowPanes);
// -----------------
// FILE PANE
// -----------------
/**
* Div element to contain 'Save to File' pane.
* @var {HTMLElement} adventurejs.SaveManager#savePane_File
* @default {}
*/
this.savePane_File = document.createElement("div");
this.savePane_File.classList.add("save_pane_file", "save_pane", "active");
this.savePane_File.innerHTML += "";
this.savePane_File.name = "savePane_File";
this.savePane_File.manager = this;
this.savePanes.push(this.savePane_File);
// check that this browser supports the methods
// needed for save/restore of local files
this.fileReaderUnsupported =
"undefined" === typeof window.File ||
"undefined" === typeof window.FileReader ||
"undefined" === typeof window.FileList ||
"undefined" === typeof window.Blob;
var a = document.createElement("a");
this.downloadUnsupported = "undefined" === typeof a.download;
//document.removeChild(a);
if (this.fileReaderUnsupported || this.downloadUnsupported) {
this.savePane_File.classList.add("unsupported_feature");
}
this.savePane_File_QA = document.createElement("div");
this.savePane_File_QA.classList = "save_pane_qa";
this.savePane_File.appendChild(this.savePane_File_QA);
// TODO accessibility
// https://getbootstrap.com/docs/4.0/components/collapse/#accessibility
this.savePane_File_QA.innerHTML =
"" +
"<p class='save_pane_qa_q'>What's this?</p>" +
"<p class='save_pane_qa_a'>" +
'The <span class="bootstrap_blue ">Save to File</span> ' +
"option will download a saved game file to your computer's hard drive. " +
"<br/><br/>PRO: You can restore these save files in any browser that can run " +
"this game, and copy them to other computers. " +
"<br/><br/>CON: For reasons of security, the browser won't open a file dialog " +
"to let you choose where to put it. " +
"It should go to your browser's default Download folder. " +
"</p>";
this.savePane_File_QA.addEventListener("click", function () {
this.classList.toggle("active");
});
this.saveRowPanes.appendChild(this.savePane_File);
this.saveTab_File.savePane = this.savePane_File;
this.saveTab_File.savePaneQA = this.savePane_File_QA;
// SAVE BUTTON
this.saveButtonContainer_File = document.createElement("div");
this.saveButtonContainer_File.classList.add(
"save_button_container_file",
"save_button_container",
"supported"
);
this.savePane_File.appendChild(this.saveButtonContainer_File);
this.saveButton_File = document.createElement("button");
this.saveButton_File.classList.add("save_button_file", "save_button");
this.saveButton_File.type = "button";
this.saveButton_File.value = "Save to File";
this.saveButton_File.innerHTML = "Save to File";
this.saveButton_File.name = "saveButton_File";
this.saveButton_File.manager = this;
this.saveButtonContainer_File.appendChild(this.saveButton_File);
this.saveButton_File.addEventListener("click", function () {
this.manager.clickSaveButton_File();
});
this.saveButtons.push(this.saveButton_File);
// UNSUPPORTED MESSAGE
this.saveWarningContainer_File = document.createElement("div");
this.saveWarningContainer_File.classList.add(
"save_warning_container_file",
"save_warning_container",
"unsupported"
);
this.savePane_File.appendChild(this.saveWarningContainer_File);
this.saveWarning_File = document.createElement("div");
this.saveWarning_File.classList =
"restore_pane_warning alert alert-danger";
this.saveWarningContainer_File.appendChild(this.saveWarning_File);
this.saveWarning_File.innerHTML =
"" +
"<p class='save_pane_warning'>" +
"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>";
// -----------------
// BROWSER PANE
// -----------------
/**
* Div element to contain 'Save to Browser' pane.
* @var {HTMLElement} adventurejs.SaveManager#savePane_Browser
* @default {}
*/
this.savePane_Browser = document.createElement("div");
this.savePane_Browser.classList.add("save_pane_browser", "save_pane");
this.savePane_Browser.name = "savePane_Browser";
this.savePane_Browser.manager = this;
this.savePanes.push(this.savePane_Browser);
this.savePane_Browser_QA = document.createElement("div");
this.savePane_Browser_QA.classList = "save_pane_qa";
this.savePane_Browser.appendChild(this.savePane_Browser_QA);
// TODO accessibility
// https://getbootstrap.com/docs/4.0/components/collapse/#accessibility
this.savePane_Browser_QA.innerHTML =
"" +
"<p class='save_pane_qa_q'>What's this?</p>" +
"<p class='save_pane_qa_a'>" +
'The <span class="bootstrap_blue ">Save to Browser</span> ' +
"option will let you save your game to your web browser's " +
'<span class="bootstrap_blue ">Local Storage</span>. ' +
"It's like a cookie, but bigger. " +
"<br/><br/>PRO: Easy to manage saves if you " +
"only play the game in this browser on this computer. " +
"Best for short games that you're unlikely to replay. " +
"<br/><br/>CON: Storage is limited. You can only access these saves from " +
"this browser on this computer. " +
"If you clear your browser's cache, you may erase your saved games. " +
"</p>";
this.savePane_Browser_QA.addEventListener("click", function () {
this.classList.toggle("active");
});
this.saveRowPanes.appendChild(this.savePane_Browser);
this.saveTab_Browser.savePane = this.savePane_Browser;
this.saveTab_Browser.savePaneQA = this.savePane_Browser_QA;
// SAVE BUTTON
this.saveButtonContainer_Browser = document.createElement("div");
this.saveButtonContainer_Browser.classList.add(
"save_button_container_browser",
"save_button_container"
);
this.savePane_Browser.appendChild(this.saveButtonContainer_Browser);
this.saveButton_Browser = document.createElement("button");
this.saveButton_Browser.classList.add(
"save_button_browser",
"save_button"
);
this.saveButton_Browser.type = "button";
this.saveButton_Browser.value = "Save to Browser";
this.saveButton_Browser.innerHTML = "Save to Browser";
this.saveButton_Browser.name = "saveButton_Browser";
this.saveButton_Browser.manager = this;
this.saveButtonContainer_Browser.appendChild(this.saveButton_Browser);
this.saveButton_Browser.addEventListener("click", function () {
this.manager.clickButton_Browser();
});
this.saveButtons.push(this.saveButton_Browser);
// -----------------
// SERVER PANE
// -----------------
/**
* Div element to contain 'Save to Server' pane.
* @var {HTMLElement} adventurejs.SaveManager#savePane_Server
* @default {}
*/
this.savePane_Server = document.createElement("div");
this.savePane_Server.classList.add("save_pane_server", "save_pane");
this.savePane_Server.name = "savePane_Server";
this.savePane_Server.manager = this;
this.savePanes.push(this.savePane_Server);
this.savePane_Server_QA = document.createElement("div");
this.savePane_Server_QA.classList = "save_pane_qa";
this.savePane_Server.appendChild(this.savePane_Server_QA);
// TODO accessibility
// https://getbootstrap.com/docs/4.0/components/collapse/#accessibility
this.savePane_Server_QA.innerHTML =
"" +
"<p class='save_pane_qa_q'>What's this?</p>" +
"<p class='save_pane_qa_a'>" +
'The <span class="bootstrap_blue ">Save to Server</span> ' +
"option will let you save your game to the adventurejs.com web server. " +
"<br/><br/>PRO: It's easy to manage your saved games, and you can " +
"restore them to any browser on any computer that runs this game." +
"<br/><br/>CON: Saved games will be unavailable while you are offline." +
"</p>";
this.savePane_Server_QA.addEventListener("click", function () {
this.classList.toggle("active");
});
this.saveRowPanes.appendChild(this.savePane_Server);
this.saveTab_Server.savePane = this.savePane_Server;
this.saveTab_Server.savePaneQA = this.savePane_Server_QA;
// SAVE BUTTON
this.saveButtonContainer_Server = document.createElement("div");
this.saveButtonContainer_Server.classList.add(
"save_button_container_server",
"save_button_container"
);
this.savePane_Server.appendChild(this.saveButtonContainer_Server);
this.saveButton_Server = document.createElement("button");
this.saveButton_Server.classList.add("save_button_server", "save_button");
this.saveButton_Server.type = "button";
this.saveButton_Server.value = "Save to Server";
this.saveButton_Server.innerHTML = "Save to Server";
this.saveButton_Server.name = "saveButton_Server";
this.saveButton_Server.manager = this;
this.saveButtonContainer_Server.appendChild(this.saveButton_Server);
this.saveButton_Server.addEventListener("click", function () {
this.manager.clickButton_Server();
});
this.saveButtons.push(this.saveButton_Server);
}
/**
* Open the Save pop-up window.
* @memberOf adventurejs.SaveManager
* @method adventurejs.SaveManager#openDisplay
* @kind function
*/
openDisplay() {
this.saveDisplay.classList.add("active");
// set a default save name
this.saveInput.value =
this.game.titleSerialized + "_" + new Date().getTime().toString();
// delay because the select doesn't work without it
setTimeout(
function (saveManager) {
saveManager.saveInput.select();
},
50,
this
);
}
/**
* Close the Save pop-up window.
* @memberOf adventurejs.SaveManager
* @method adventurejs.SaveManager#closeDisplay
* @kind function
*/
closeDisplay() {
this.saveDisplay.classList.remove("active");
this.selectTab(this.saveTab_File);
this.saveButtons.forEach(function (button) {
button.classList.add("inactive");
});
}
/**
* Function that gets called by the Close button.
* @memberOf adventurejs.SaveManager
* @method adventurejs.SaveManager#clickButton_Close
* @kind function
*/
clickButton_Close() {
console.log("clickButton_Close");
this.closeDisplay();
var msg = "Save cancelled.";
this.game.print(msg);
return;
}
/**
* Save to file.
* @memberOf adventurejs.SaveManager
* @method adventurejs.SaveManager#clickSaveButton_File
* @kind function
* @todo Investigate FileSaver.js
*/
clickSaveButton_File() {
console.log("clickSaveButton_File");
var inputFileName = this.sanitizeInputFileName();
// DEPRECATED pulled last snapshot from undo history
//var text = this.game.world_history[0];
// NEW get diff between baseline and current
var text = A.getBaselineDiff.call(this.game);
var blob = new Blob([text], { type: "application/json" });
var anchor = document.createElement("a");
anchor.download = inputFileName + ".json";
anchor.href = (window.webkitURL || window.URL).createObjectURL(blob);
anchor.dataset.downloadurl = [
"application/json",
anchor.download,
anchor.href,
].join(":");
anchor.click();
URL.revokeObjectURL(anchor.href);
this.closeDisplay();
var msg =
"Game saved! " +
"<span class='text-success'>" +
inputFileName +
".json" +
"</span> " +
"has been saved to your Downloads folder.";
this.game.print(msg);
return true;
}
/**
* Save game to browser cookie.
* <br><br>
* Player can name their save however they want. However, we always
* prepend the game name because local storage applies
* to all pages on a given domain, meaning that all saves for all
* games played at, i.e., adventurejs.com will be saved in the same
* local storage. And, local storage appears not to have any method
* for nesting data, so we can't have a parent object or 'folder'.
* Instead, we need to name them in such a way that we
* can identify them by game.
* @memberOf adventurejs.SaveManager
* @method adventurejs.SaveManager#clickButton_Browser
* @kind function
*/
clickButton_Browser() {
console.log("clickButton_Browser");
var inputFileName = this.sanitizeInputFileName();
//if( this.game.titleSerialized !== inputFileName.substr( 0, this.game.titleSerialized.length ) ) // @deprecated
if (
this.game.titleSerialized !==
inputFileName.substring(0, this.game.titleSerialized.length)
) {
inputFileName = this.game.titleSerialized + "_" + inputFileName;
}
var storage = window.localStorage;
// DEPRECATED pulled last snapshot from undo history
//storage.setItem( inputFileName, this.game.world_history[0] );
// NEW get diff between baseline and current
storage.setItem(inputFileName, A.getBaselineDiff.call(this.game));
this.closeDisplay();
var msg = "Game saved to your browser's Local Storage!";
this.game.print(msg);
return true;
}
/**
* Save game to server.
* @memberOf adventurejs.SaveManager
* @method adventurejs.SaveManager#clickButton_Server
* @kind function
* @todo Everything.
*/
clickButton_Server() {
console.log("clickButton_Server");
var inputFileName = this.sanitizeInputFileName();
// TODO ...
}
/**
* OnInput or OnKeyDown, activate or deactivate save buttons
* depending on state of input field. Deactivate if empty,
* otherwise activate.
* @memberOf adventurejs.SaveManager
* @method adventurejs.SaveManager#
* @kind function
*/
saveInputOninput(saveInput) {
var activeState = saveInput.value === "" ? "add" : "remove";
//var buttons = document.querySelectorAll('.save_tab');
this.saveButtons.forEach(function (saveButton) {
saveButton.classList[activeState]("inactive");
}, activeState);
}
// LOCAL STORAGE ???
// can do a saved file browser for cookies...& maybe server...& local storage?
// go back to cookie/local/server buttons as tabs?
/**
* Sanitize input file name. Permitted characters are
* a-z, A-Z, 0-9, accented vowels áéíóúñü, _underscore, and -hyphen.
* @memberOf adventurejs.SaveManager
* @method adventurejs.SaveManager#sanitizeInputFileName
* @kind function
*/
sanitizeInputFileName() {
console.log("validate input");
console.log("this.saveInput.value", this.saveInput.value);
var inputFileName = this.saveInput.value;
inputFileName = inputFileName.replace(/ /gim, "_");
inputFileName = inputFileName.replace(/[^a-zA-Z0-9áéíóúñü_-]/gim, "");
if (!inputFileName) {
inputFileName =
this.game.titleSerialized + "_" + new Date().getTime().toString();
}
return inputFileName;
}
/**
* Make a selected tab active.
* @memberOf adventurejs.SaveManager
* @method adventurejs.SaveManager#selectTab
* @kind function
*/
selectTab(selectedTab) {
this.saveTabs.forEach(function (tabButton) {
if (tabButton !== selectedTab) {
tabButton.classList.remove("active");
tabButton.savePane.classList.remove("active");
tabButton.savePaneQA.classList.remove("active");
} else {
tabButton.classList.add("active");
tabButton.savePane.classList.add("active");
}
}, selectedTab);
}
/**
* Provides a chainable shortcut method for setting a number of properties on the instance.
* @method adventurejs.SaveManager#set
* @param {Object} props A generic object containing properties to copy to the DisplayObject instance.
* @returns {adventurejs.SaveManager} 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.SaveManager = SaveManager;
})();