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