Pre-release
Adventure.js Docs Downloads
Score: 0 Moves: 0
// 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;
})();